Skip to content

Commit 6aab7ee

Browse files
Merge pull request #52 from modelcontextprotocol/justin/capabilities-negotiation
Test capabilities, use explicit specifiers for now
2 parents c82d633 + 70cfb0f commit 6aab7ee

File tree

9 files changed

+1001
-101
lines changed

9 files changed

+1001
-101
lines changed

src/cli.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ import { StdioServerTransport } from "./server/stdio.js";
1717
import { ListResourcesResultSchema } from "./types.js";
1818

1919
async function runClient(url_or_command: string, args: string[]) {
20-
const client = new Client({
21-
name: "mcp-typescript test client",
22-
version: "0.1.0",
23-
});
20+
const client = new Client(
21+
{
22+
name: "mcp-typescript test client",
23+
version: "0.1.0",
24+
},
25+
{
26+
capabilities: {
27+
sampling: {},
28+
},
29+
},
30+
);
2431

2532
let clientTransport;
2633

@@ -63,10 +70,15 @@ async function runServer(port: number | null) {
6370
console.log("Got new SSE connection");
6471

6572
const transport = new SSEServerTransport("/message", res);
66-
const server = new Server({
67-
name: "mcp-typescript test server",
68-
version: "0.1.0",
69-
});
73+
const server = new Server(
74+
{
75+
name: "mcp-typescript test server",
76+
version: "0.1.0",
77+
},
78+
{
79+
capabilities: {},
80+
},
81+
);
7082

7183
servers.push(server);
7284

@@ -97,10 +109,20 @@ async function runServer(port: number | null) {
97109
console.log(`Server running on http://localhost:${port}/sse`);
98110
});
99111
} else {
100-
const server = new Server({
101-
name: "mcp-typescript test server",
102-
version: "0.1.0",
103-
});
112+
const server = new Server(
113+
{
114+
name: "mcp-typescript test server",
115+
version: "0.1.0",
116+
},
117+
{
118+
capabilities: {
119+
prompts: {},
120+
resources: {},
121+
tools: {},
122+
logging: {},
123+
},
124+
},
125+
);
104126

105127
const transport = new StdioServerTransport();
106128
await server.connect(transport);

src/client/index.test.ts

Lines changed: 255 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ import {
99
ResultSchema,
1010
LATEST_PROTOCOL_VERSION,
1111
SUPPORTED_PROTOCOL_VERSIONS,
12+
InitializeRequestSchema,
13+
ListResourcesRequestSchema,
14+
ListToolsRequestSchema,
15+
CreateMessageRequestSchema,
16+
ListRootsRequestSchema,
1217
} from "../types.js";
1318
import { Transport } from "../shared/transport.js";
19+
import { Server } from "../server/index.js";
20+
import { InMemoryTransport } from "../inMemory.js";
1421

1522
test("should initialize with matching protocol version", async () => {
1623
const clientTransport: Transport = {
@@ -35,10 +42,17 @@ test("should initialize with matching protocol version", async () => {
3542
}),
3643
};
3744

38-
const client = new Client({
39-
name: "test client",
40-
version: "1.0",
41-
});
45+
const client = new Client(
46+
{
47+
name: "test client",
48+
version: "1.0",
49+
},
50+
{
51+
capabilities: {
52+
sampling: {},
53+
},
54+
},
55+
);
4256

4357
await client.connect(clientTransport);
4458

@@ -77,10 +91,17 @@ test("should initialize with supported older protocol version", async () => {
7791
}),
7892
};
7993

80-
const client = new Client({
81-
name: "test client",
82-
version: "1.0",
83-
});
94+
const client = new Client(
95+
{
96+
name: "test client",
97+
version: "1.0",
98+
},
99+
{
100+
capabilities: {
101+
sampling: {},
102+
},
103+
},
104+
);
84105

85106
await client.connect(clientTransport);
86107

@@ -114,10 +135,17 @@ test("should reject unsupported protocol version", async () => {
114135
}),
115136
};
116137

117-
const client = new Client({
118-
name: "test client",
119-
version: "1.0",
120-
});
138+
const client = new Client(
139+
{
140+
name: "test client",
141+
version: "1.0",
142+
},
143+
{
144+
capabilities: {
145+
sampling: {},
146+
},
147+
},
148+
);
121149

122150
await expect(client.connect(clientTransport)).rejects.toThrow(
123151
"Server's protocol version is not supported: invalid-version",
@@ -126,6 +154,210 @@ test("should reject unsupported protocol version", async () => {
126154
expect(clientTransport.close).toHaveBeenCalled();
127155
});
128156

157+
test("should respect server capabilities", async () => {
158+
const server = new Server(
159+
{
160+
name: "test server",
161+
version: "1.0",
162+
},
163+
{
164+
capabilities: {
165+
resources: {},
166+
tools: {},
167+
},
168+
},
169+
);
170+
171+
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
172+
protocolVersion: LATEST_PROTOCOL_VERSION,
173+
capabilities: {
174+
resources: {},
175+
tools: {},
176+
},
177+
serverInfo: {
178+
name: "test",
179+
version: "1.0",
180+
},
181+
}));
182+
183+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
184+
resources: [],
185+
}));
186+
187+
server.setRequestHandler(ListToolsRequestSchema, () => ({
188+
tools: [],
189+
}));
190+
191+
const [clientTransport, serverTransport] =
192+
InMemoryTransport.createLinkedPair();
193+
194+
const client = new Client(
195+
{
196+
name: "test client",
197+
version: "1.0",
198+
},
199+
{
200+
capabilities: {
201+
sampling: {},
202+
},
203+
enforceStrictCapabilities: true,
204+
},
205+
);
206+
207+
await Promise.all([
208+
client.connect(clientTransport),
209+
server.connect(serverTransport),
210+
]);
211+
212+
// Server supports resources and tools, but not prompts
213+
expect(client.getServerCapabilities()).toEqual({
214+
resources: {},
215+
tools: {},
216+
});
217+
218+
// These should work
219+
await expect(client.listResources()).resolves.not.toThrow();
220+
await expect(client.listTools()).resolves.not.toThrow();
221+
222+
// This should throw because prompts are not supported
223+
await expect(client.listPrompts()).rejects.toThrow(
224+
"Server does not support prompts",
225+
);
226+
});
227+
228+
test("should respect client notification capabilities", async () => {
229+
const server = new Server(
230+
{
231+
name: "test server",
232+
version: "1.0",
233+
},
234+
{
235+
capabilities: {},
236+
},
237+
);
238+
239+
const client = new Client(
240+
{
241+
name: "test client",
242+
version: "1.0",
243+
},
244+
{
245+
capabilities: {
246+
roots: {
247+
listChanged: true,
248+
},
249+
},
250+
},
251+
);
252+
253+
const [clientTransport, serverTransport] =
254+
InMemoryTransport.createLinkedPair();
255+
256+
await Promise.all([
257+
client.connect(clientTransport),
258+
server.connect(serverTransport),
259+
]);
260+
261+
// This should work because the client has the roots.listChanged capability
262+
await expect(client.sendRootsListChanged()).resolves.not.toThrow();
263+
264+
// Create a new client without the roots.listChanged capability
265+
const clientWithoutCapability = new Client(
266+
{
267+
name: "test client without capability",
268+
version: "1.0",
269+
},
270+
{
271+
capabilities: {},
272+
enforceStrictCapabilities: true,
273+
},
274+
);
275+
276+
await clientWithoutCapability.connect(clientTransport);
277+
278+
// This should throw because the client doesn't have the roots.listChanged capability
279+
await expect(clientWithoutCapability.sendRootsListChanged()).rejects.toThrow(
280+
/^Client does not support/,
281+
);
282+
});
283+
284+
test("should respect server notification capabilities", async () => {
285+
const server = new Server(
286+
{
287+
name: "test server",
288+
version: "1.0",
289+
},
290+
{
291+
capabilities: {
292+
logging: {},
293+
resources: {
294+
listChanged: true,
295+
},
296+
},
297+
},
298+
);
299+
300+
const client = new Client(
301+
{
302+
name: "test client",
303+
version: "1.0",
304+
},
305+
{
306+
capabilities: {},
307+
},
308+
);
309+
310+
const [clientTransport, serverTransport] =
311+
InMemoryTransport.createLinkedPair();
312+
313+
await Promise.all([
314+
client.connect(clientTransport),
315+
server.connect(serverTransport),
316+
]);
317+
318+
// These should work because the server has the corresponding capabilities
319+
await expect(
320+
server.sendLoggingMessage({ level: "info", data: "Test" }),
321+
).resolves.not.toThrow();
322+
await expect(server.sendResourceListChanged()).resolves.not.toThrow();
323+
324+
// This should throw because the server doesn't have the tools capability
325+
await expect(server.sendToolListChanged()).rejects.toThrow(
326+
"Server does not support notifying of tool list changes",
327+
);
328+
});
329+
330+
test("should only allow setRequestHandler for declared capabilities", () => {
331+
const client = new Client(
332+
{
333+
name: "test client",
334+
version: "1.0",
335+
},
336+
{
337+
capabilities: {
338+
sampling: {},
339+
},
340+
},
341+
);
342+
343+
// This should work because sampling is a declared capability
344+
expect(() => {
345+
client.setRequestHandler(CreateMessageRequestSchema, () => ({
346+
model: "test-model",
347+
role: "assistant",
348+
content: {
349+
type: "text",
350+
text: "Test response",
351+
},
352+
}));
353+
}).not.toThrow();
354+
355+
// This should throw because roots listing is not a declared capability
356+
expect(() => {
357+
client.setRequestHandler(ListRootsRequestSchema, () => ({}));
358+
}).toThrow("Client does not support roots capability");
359+
});
360+
129361
/*
130362
Test that custom request/notification/result schemas can be used with the Client class.
131363
*/
@@ -171,10 +403,17 @@ test("should typecheck", () => {
171403
WeatherRequest,
172404
WeatherNotification,
173405
WeatherResult
174-
>({
175-
name: "WeatherClient",
176-
version: "1.0.0",
177-
});
406+
>(
407+
{
408+
name: "WeatherClient",
409+
version: "1.0.0",
410+
},
411+
{
412+
capabilities: {
413+
sampling: {},
414+
},
415+
},
416+
);
178417

179418
// Typecheck that only valid weather requests/notifications/results are allowed
180419
false &&

0 commit comments

Comments
 (0)