Skip to content

Commit 438505b

Browse files
Merge pull request #117 from modelcontextprotocol/justin/simplified-api
Simplified, Express-like API
2 parents 405ee78 + e8a5ffc commit 438505b

14 files changed

+3427
-86
lines changed

README.md

+358-58
Large diffs are not rendered by default.

package-lock.json

+31-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -48,7 +48,8 @@
4848
"dependencies": {
4949
"content-type": "^1.0.5",
5050
"raw-body": "^3.0.0",
51-
"zod": "^3.23.8"
51+
"zod": "^3.23.8",
52+
"zod-to-json-schema": "^3.24.1"
5253
},
5354
"devDependencies": {
5455
"@eslint/js": "^9.8.0",

src/client/index.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
mergeCapabilities,
23
Protocol,
34
ProtocolOptions,
45
RequestOptions,
@@ -44,7 +45,7 @@ export type ClientOptions = ProtocolOptions & {
4445
/**
4546
* Capabilities to advertise as being supported by this client.
4647
*/
47-
capabilities: ClientCapabilities;
48+
capabilities?: ClientCapabilities;
4849
};
4950

5051
/**
@@ -90,10 +91,25 @@ export class Client<
9091
*/
9192
constructor(
9293
private _clientInfo: Implementation,
93-
options: ClientOptions,
94+
options?: ClientOptions,
9495
) {
9596
super(options);
96-
this._capabilities = options.capabilities;
97+
this._capabilities = options?.capabilities ?? {};
98+
}
99+
100+
/**
101+
* Registers new capabilities. This can only be called before connecting to a transport.
102+
*
103+
* The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
104+
*/
105+
public registerCapabilities(capabilities: ClientCapabilities): void {
106+
if (this.transport) {
107+
throw new Error(
108+
"Cannot register capabilities after connecting to transport",
109+
);
110+
}
111+
112+
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
97113
}
98114

99115
protected assertCapability(

src/server/completable.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { z } from "zod";
2+
import { completable } from "./completable.js";
3+
4+
describe("completable", () => {
5+
it("preserves types and values of underlying schema", () => {
6+
const baseSchema = z.string();
7+
const schema = completable(baseSchema, () => []);
8+
9+
expect(schema.parse("test")).toBe("test");
10+
expect(() => schema.parse(123)).toThrow();
11+
});
12+
13+
it("provides access to completion function", async () => {
14+
const completions = ["foo", "bar", "baz"];
15+
const schema = completable(z.string(), () => completions);
16+
17+
expect(await schema._def.complete("")).toEqual(completions);
18+
});
19+
20+
it("allows async completion functions", async () => {
21+
const completions = ["foo", "bar", "baz"];
22+
const schema = completable(z.string(), async () => completions);
23+
24+
expect(await schema._def.complete("")).toEqual(completions);
25+
});
26+
27+
it("passes current value to completion function", async () => {
28+
const schema = completable(z.string(), (value) => [value + "!"]);
29+
30+
expect(await schema._def.complete("test")).toEqual(["test!"]);
31+
});
32+
33+
it("works with number schemas", async () => {
34+
const schema = completable(z.number(), () => [1, 2, 3]);
35+
36+
expect(schema.parse(1)).toBe(1);
37+
expect(await schema._def.complete(0)).toEqual([1, 2, 3]);
38+
});
39+
40+
it("preserves schema description", () => {
41+
const desc = "test description";
42+
const schema = completable(z.string().describe(desc), () => []);
43+
44+
expect(schema.description).toBe(desc);
45+
});
46+
});

src/server/completable.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
ZodTypeAny,
3+
ZodTypeDef,
4+
ZodType,
5+
ParseInput,
6+
ParseReturnType,
7+
RawCreateParams,
8+
ZodErrorMap,
9+
ProcessedCreateParams,
10+
} from "zod";
11+
12+
export enum McpZodTypeKind {
13+
Completable = "McpCompletable",
14+
}
15+
16+
export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
17+
value: T["_input"],
18+
) => T["_input"][] | Promise<T["_input"][]>;
19+
20+
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny>
21+
extends ZodTypeDef {
22+
type: T;
23+
complete: CompleteCallback<T>;
24+
typeName: McpZodTypeKind.Completable;
25+
}
26+
27+
export class Completable<T extends ZodTypeAny> extends ZodType<
28+
T["_output"],
29+
CompletableDef<T>,
30+
T["_input"]
31+
> {
32+
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
33+
const { ctx } = this._processInputParams(input);
34+
const data = ctx.data;
35+
return this._def.type._parse({
36+
data,
37+
path: ctx.path,
38+
parent: ctx,
39+
});
40+
}
41+
42+
unwrap() {
43+
return this._def.type;
44+
}
45+
46+
static create = <T extends ZodTypeAny>(
47+
type: T,
48+
params: RawCreateParams & {
49+
complete: CompleteCallback<T>;
50+
},
51+
): Completable<T> => {
52+
return new Completable({
53+
type,
54+
typeName: McpZodTypeKind.Completable,
55+
complete: params.complete,
56+
...processCreateParams(params),
57+
});
58+
};
59+
}
60+
61+
/**
62+
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
63+
*/
64+
export function completable<T extends ZodTypeAny>(
65+
schema: T,
66+
complete: CompleteCallback<T>,
67+
): Completable<T> {
68+
return Completable.create(schema, { ...schema._def, complete });
69+
}
70+
71+
// Not sure why this isn't exported from Zod:
72+
// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130
73+
function processCreateParams(params: RawCreateParams): ProcessedCreateParams {
74+
if (!params) return {};
75+
const { errorMap, invalid_type_error, required_error, description } = params;
76+
if (errorMap && (invalid_type_error || required_error)) {
77+
throw new Error(
78+
`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`,
79+
);
80+
}
81+
if (errorMap) return { errorMap: errorMap, description };
82+
const customMap: ZodErrorMap = (iss, ctx) => {
83+
const { message } = params;
84+
85+
if (iss.code === "invalid_enum_value") {
86+
return { message: message ?? ctx.defaultError };
87+
}
88+
if (typeof ctx.data === "undefined") {
89+
return { message: message ?? required_error ?? ctx.defaultError };
90+
}
91+
if (iss.code !== "invalid_type") return { message: ctx.defaultError };
92+
return { message: message ?? invalid_type_error ?? ctx.defaultError };
93+
};
94+
return { errorMap: customMap, description };
95+
}

src/server/index.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ test("should handle server cancelling a request", async () => {
478478
// Request should be rejected
479479
await expect(createMessagePromise).rejects.toBe("Cancelled by test");
480480
});
481+
481482
test("should handle request timeout", async () => {
482483
const server = new Server(
483484
{

src/server/index.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
mergeCapabilities,
23
Protocol,
34
ProtocolOptions,
45
RequestOptions,
@@ -32,7 +33,7 @@ export type ServerOptions = ProtocolOptions & {
3233
/**
3334
* Capabilities to advertise as being supported by this server.
3435
*/
35-
capabilities: ServerCapabilities;
36+
capabilities?: ServerCapabilities;
3637

3738
/**
3839
* Optional instructions describing how to use the server and its features.
@@ -89,11 +90,11 @@ export class Server<
8990
*/
9091
constructor(
9192
private _serverInfo: Implementation,
92-
options: ServerOptions,
93+
options?: ServerOptions,
9394
) {
9495
super(options);
95-
this._capabilities = options.capabilities;
96-
this._instructions = options.instructions;
96+
this._capabilities = options?.capabilities ?? {};
97+
this._instructions = options?.instructions;
9798

9899
this.setRequestHandler(InitializeRequestSchema, (request) =>
99100
this._oninitialize(request),
@@ -103,6 +104,21 @@ export class Server<
103104
);
104105
}
105106

107+
/**
108+
* Registers new capabilities. This can only be called before connecting to a transport.
109+
*
110+
* The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
111+
*/
112+
public registerCapabilities(capabilities: ServerCapabilities): void {
113+
if (this.transport) {
114+
throw new Error(
115+
"Cannot register capabilities after connecting to transport",
116+
);
117+
}
118+
119+
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
120+
}
121+
106122
protected assertCapabilityForMethod(method: RequestT["method"]): void {
107123
switch (method as ServerRequest["method"]) {
108124
case "sampling/createMessage":

0 commit comments

Comments
 (0)