Skip to content

Commit dc45f5d

Browse files
committed
Handle capabilities checks at the request() level, make non-strict by default
1 parent 8e369b7 commit dc45f5d

File tree

7 files changed

+254
-83
lines changed

7 files changed

+254
-83
lines changed

src/cli.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ async function runClient(url_or_command: string, args: string[]) {
2323
version: "0.1.0",
2424
},
2525
{
26-
sampling: {},
26+
capabilities: {
27+
sampling: {},
28+
},
2729
},
2830
);
2931

@@ -73,7 +75,9 @@ async function runServer(port: number | null) {
7375
name: "mcp-typescript test server",
7476
version: "0.1.0",
7577
},
76-
{},
78+
{
79+
capabilities: {},
80+
},
7781
);
7882

7983
servers.push(server);
@@ -111,10 +115,12 @@ async function runServer(port: number | null) {
111115
version: "0.1.0",
112116
},
113117
{
114-
prompts: {},
115-
resources: {},
116-
tools: {},
117-
logging: {},
118+
capabilities: {
119+
prompts: {},
120+
resources: {},
121+
tools: {},
122+
logging: {},
123+
},
118124
},
119125
);
120126

src/client/index.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ test("should initialize with matching protocol version", async () => {
4646
version: "1.0",
4747
},
4848
{
49-
sampling: {},
49+
capabilities: {
50+
sampling: {},
51+
},
5052
},
5153
);
5254

@@ -93,7 +95,9 @@ test("should initialize with supported older protocol version", async () => {
9395
version: "1.0",
9496
},
9597
{
96-
sampling: {},
98+
capabilities: {
99+
sampling: {},
100+
},
97101
},
98102
);
99103

@@ -135,7 +139,9 @@ test("should reject unsupported protocol version", async () => {
135139
version: "1.0",
136140
},
137141
{
138-
sampling: {},
142+
capabilities: {
143+
sampling: {},
144+
},
139145
},
140146
);
141147

@@ -153,8 +159,10 @@ test("should respect server capabilities", async () => {
153159
version: "1.0",
154160
},
155161
{
156-
resources: {},
157-
tools: {},
162+
capabilities: {
163+
resources: {},
164+
tools: {},
165+
},
158166
},
159167
);
160168

@@ -187,7 +195,10 @@ test("should respect server capabilities", async () => {
187195
version: "1.0",
188196
},
189197
{
190-
sampling: {},
198+
capabilities: {
199+
sampling: {},
200+
},
201+
enforceStrictCapabilities: true,
191202
},
192203
);
193204

@@ -263,7 +274,9 @@ test("should typecheck", () => {
263274
version: "1.0.0",
264275
},
265276
{
266-
sampling: {},
277+
capabilities: {
278+
sampling: {},
279+
},
267280
},
268281
);
269282

src/client/index.ts

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ProgressCallback, Protocol } from "../shared/protocol.js";
1+
import {
2+
ProgressCallback,
3+
Protocol,
4+
ProtocolOptions,
5+
} from "../shared/protocol.js";
26
import { Transport } from "../shared/transport.js";
37
import {
48
CallToolRequest,
@@ -36,6 +40,13 @@ import {
3640
UnsubscribeRequest,
3741
} from "../types.js";
3842

43+
export type ClientOptions = ProtocolOptions & {
44+
/**
45+
* Capabilities to advertise as being supported by this client.
46+
*/
47+
capabilities: ClientCapabilities;
48+
};
49+
3950
/**
4051
* An MCP client on top of a pluggable transport.
4152
*
@@ -72,15 +83,28 @@ export class Client<
7283
> {
7384
private _serverCapabilities?: ServerCapabilities;
7485
private _serverVersion?: Implementation;
86+
private _capabilities: ClientCapabilities;
7587

7688
/**
7789
* Initializes this client with the given name and version information.
7890
*/
7991
constructor(
8092
private _clientInfo: Implementation,
81-
private _capabilities: ClientCapabilities,
93+
options: ClientOptions,
8294
) {
83-
super();
95+
super(options);
96+
this._capabilities = options.capabilities;
97+
}
98+
99+
protected assertCapability(
100+
capability: keyof ServerCapabilities,
101+
method: string,
102+
): void {
103+
if (!this._serverCapabilities?.[capability]) {
104+
throw new Error(
105+
`Server does not support ${capability} (required for ${method})`,
106+
);
107+
}
84108
}
85109

86110
override async connect(transport: Transport): Promise<void> {
@@ -136,14 +160,69 @@ export class Client<
136160
return this._serverVersion;
137161
}
138162

139-
private assertCapability(
140-
capability: keyof ServerCapabilities,
141-
method: string,
142-
) {
143-
if (!this._serverCapabilities?.[capability]) {
144-
throw new Error(
145-
`Server does not support ${capability} (required for ${method})`,
146-
);
163+
protected assertCapabilityForMethod(method: RequestT["method"]): void {
164+
switch (method as ClientRequest["method"]) {
165+
case "logging/setLevel":
166+
if (!this._serverCapabilities?.logging) {
167+
throw new Error(
168+
"Server does not support logging (required for logging/setLevel)",
169+
);
170+
}
171+
break;
172+
173+
case "prompts/get":
174+
case "prompts/list":
175+
if (!this._serverCapabilities?.prompts) {
176+
throw new Error(
177+
`Server does not support prompts (required for ${method})`,
178+
);
179+
}
180+
break;
181+
182+
case "resources/list":
183+
case "resources/templates/list":
184+
case "resources/read":
185+
case "resources/subscribe":
186+
case "resources/unsubscribe":
187+
if (!this._serverCapabilities?.resources) {
188+
throw new Error(
189+
`Server does not support resources (required for ${method})`,
190+
);
191+
}
192+
193+
if (
194+
method === "resources/subscribe" &&
195+
!this._serverCapabilities.resources.subscribe
196+
) {
197+
throw new Error("Server does not support resource subscriptions");
198+
}
199+
200+
break;
201+
202+
case "tools/call":
203+
case "tools/list":
204+
if (!this._serverCapabilities?.tools) {
205+
throw new Error(
206+
`Server does not support tools (required for ${method})`,
207+
);
208+
}
209+
break;
210+
211+
case "completion/complete":
212+
if (!this._serverCapabilities?.prompts) {
213+
throw new Error(
214+
"Server does not support prompts (required for completion/complete)",
215+
);
216+
}
217+
break;
218+
219+
case "initialize":
220+
// No specific capability required for initialize
221+
break;
222+
223+
case "ping":
224+
// No specific capability required for ping
225+
break;
147226
}
148227
}
149228

@@ -155,7 +234,6 @@ export class Client<
155234
params: CompleteRequest["params"],
156235
onprogress?: ProgressCallback,
157236
) {
158-
this.assertCapability("prompts", "completion/complete");
159237
return this.request(
160238
{ method: "completion/complete", params },
161239
CompleteResultSchema,
@@ -164,7 +242,6 @@ export class Client<
164242
}
165243

166244
async setLoggingLevel(level: LoggingLevel) {
167-
this.assertCapability("logging", "logging/setLevel");
168245
return this.request(
169246
{ method: "logging/setLevel", params: { level } },
170247
EmptyResultSchema,
@@ -175,7 +252,6 @@ export class Client<
175252
params: GetPromptRequest["params"],
176253
onprogress?: ProgressCallback,
177254
) {
178-
this.assertCapability("prompts", "prompts/get");
179255
return this.request(
180256
{ method: "prompts/get", params },
181257
GetPromptResultSchema,
@@ -187,7 +263,6 @@ export class Client<
187263
params?: ListPromptsRequest["params"],
188264
onprogress?: ProgressCallback,
189265
) {
190-
this.assertCapability("prompts", "prompts/list");
191266
return this.request(
192267
{ method: "prompts/list", params },
193268
ListPromptsResultSchema,
@@ -199,7 +274,6 @@ export class Client<
199274
params?: ListResourcesRequest["params"],
200275
onprogress?: ProgressCallback,
201276
) {
202-
this.assertCapability("resources", "resources/list");
203277
return this.request(
204278
{ method: "resources/list", params },
205279
ListResourcesResultSchema,
@@ -211,7 +285,6 @@ export class Client<
211285
params?: ListResourceTemplatesRequest["params"],
212286
onprogress?: ProgressCallback,
213287
) {
214-
this.assertCapability("resources", "resources/templates/list");
215288
return this.request(
216289
{ method: "resources/templates/list", params },
217290
ListResourceTemplatesResultSchema,
@@ -223,7 +296,6 @@ export class Client<
223296
params: ReadResourceRequest["params"],
224297
onprogress?: ProgressCallback,
225298
) {
226-
this.assertCapability("resources", "resources/read");
227299
return this.request(
228300
{ method: "resources/read", params },
229301
ReadResourceResultSchema,
@@ -232,15 +304,13 @@ export class Client<
232304
}
233305

234306
async subscribeResource(params: SubscribeRequest["params"]) {
235-
this.assertCapability("resources", "resources/subscribe");
236307
return this.request(
237308
{ method: "resources/subscribe", params },
238309
EmptyResultSchema,
239310
);
240311
}
241312

242313
async unsubscribeResource(params: UnsubscribeRequest["params"]) {
243-
this.assertCapability("resources", "resources/unsubscribe");
244314
return this.request(
245315
{ method: "resources/unsubscribe", params },
246316
EmptyResultSchema,
@@ -254,7 +324,6 @@ export class Client<
254324
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
255325
onprogress?: ProgressCallback,
256326
) {
257-
this.assertCapability("tools", "tools/call");
258327
return this.request(
259328
{ method: "tools/call", params },
260329
resultSchema,
@@ -266,7 +335,6 @@ export class Client<
266335
params?: ListToolsRequest["params"],
267336
onprogress?: ProgressCallback,
268337
) {
269-
this.assertCapability("tools", "tools/list");
270338
return this.request(
271339
{ method: "tools/list", params },
272340
ListToolsResultSchema,

0 commit comments

Comments
 (0)