Skip to content

Commit d3ed226

Browse files
committed
- add new tool() api and tests
- fix schema validation - comment headers on tests
1 parent 7e18a7c commit d3ed226

File tree

8 files changed

+580
-403
lines changed

8 files changed

+580
-403
lines changed

package-lock.json

Lines changed: 2 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client/index.test.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { Transport } from "../shared/transport.js";
2121
import { Server } from "../server/index.js";
2222
import { InMemoryTransport } from "../inMemory.js";
2323

24+
/***
25+
* Test: Initialize with Matching Protocol Version
26+
*/
2427
test("should initialize with matching protocol version", async () => {
2528
const clientTransport: Transport = {
2629
start: jest.fn().mockResolvedValue(undefined),
@@ -76,6 +79,9 @@ test("should initialize with matching protocol version", async () => {
7679
expect(client.getInstructions()).toEqual("test instructions");
7780
});
7881

82+
/***
83+
* Test: Initialize with Supported Older Protocol Version
84+
*/
7985
test("should initialize with supported older protocol version", async () => {
8086
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
8187
const clientTransport: Transport = {
@@ -124,6 +130,9 @@ test("should initialize with supported older protocol version", async () => {
124130
expect(client.getInstructions()).toBeUndefined();
125131
});
126132

133+
/***
134+
* Test: Reject Unsupported Protocol Version
135+
*/
127136
test("should reject unsupported protocol version", async () => {
128137
const clientTransport: Transport = {
129138
start: jest.fn().mockResolvedValue(undefined),
@@ -166,6 +175,9 @@ test("should reject unsupported protocol version", async () => {
166175
expect(clientTransport.close).toHaveBeenCalled();
167176
});
168177

178+
/***
179+
* Test: Connect New Client to Old Supported Server Version
180+
*/
169181
test("should connect new client to old, supported server version", async () => {
170182
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
171183
const server = new Server(
@@ -229,6 +241,9 @@ test("should connect new client to old, supported server version", async () => {
229241
});
230242
});
231243

244+
/***
245+
* Test: Version Negotiation with Old Client and Newer Server
246+
*/
232247
test("should negotiate version when client is old, and newer server supports its version", async () => {
233248
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
234249
const server = new Server(
@@ -292,6 +307,9 @@ test("should negotiate version when client is old, and newer server supports its
292307
});
293308
});
294309

310+
/***
311+
* Test: Throw when Old Client and Server Version Mismatch
312+
*/
295313
test("should throw when client is old, and server doesn't support its version", async () => {
296314
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
297315
const FUTURE_VERSION = "FUTURE_VERSION";
@@ -354,6 +372,9 @@ test("should throw when client is old, and server doesn't support its version",
354372

355373
});
356374

375+
/***
376+
* Test: Respect Server Capabilities
377+
*/
357378
test("should respect server capabilities", async () => {
358379
const server = new Server(
359380
{
@@ -434,6 +455,9 @@ test("should respect server capabilities", async () => {
434455
).rejects.toThrow("Server does not support completions");
435456
});
436457

458+
/***
459+
* Test: Respect Client Notification Capabilities
460+
*/
437461
test("should respect client notification capabilities", async () => {
438462
const server = new Server(
439463
{
@@ -490,6 +514,9 @@ test("should respect client notification capabilities", async () => {
490514
);
491515
});
492516

517+
/***
518+
* Test: Respect Server Notification Capabilities
519+
*/
493520
test("should respect server notification capabilities", async () => {
494521
const server = new Server(
495522
{
@@ -536,6 +563,9 @@ test("should respect server notification capabilities", async () => {
536563
);
537564
});
538565

566+
/***
567+
* Test: Only Allow setRequestHandler for Declared Capabilities
568+
*/
539569
test("should only allow setRequestHandler for declared capabilities", () => {
540570
const client = new Client(
541571
{
@@ -567,9 +597,10 @@ test("should only allow setRequestHandler for declared capabilities", () => {
567597
}).toThrow("Client does not support roots capability");
568598
});
569599

570-
/*
571-
Test that custom request/notification/result schemas can be used with the Client class.
572-
*/
600+
/***
601+
* Test: Type Checking
602+
* Test that custom request/notification/result schemas can be used with the Client class.
603+
*/
573604
test("should typecheck", () => {
574605
const GetWeatherRequestSchema = RequestSchema.extend({
575606
method: z.literal("weather/get"),
@@ -646,6 +677,9 @@ test("should typecheck", () => {
646677
});
647678
});
648679

680+
/***
681+
* Test: Handle Client Cancelling a Request
682+
*/
649683
test("should handle client cancelling a request", async () => {
650684
const server = new Server(
651685
{
@@ -701,6 +735,9 @@ test("should handle client cancelling a request", async () => {
701735
await expect(listResourcesPromise).rejects.toBe("Cancelled by test");
702736
});
703737

738+
/***
739+
* Test: Handle Request Timeout
740+
*/
704741
test("should handle request timeout", async () => {
705742
const server = new Server(
706743
{
@@ -757,6 +794,9 @@ test("should handle request timeout", async () => {
757794
});
758795

759796
describe('outputSchema validation', () => {
797+
/***
798+
* Test: Validate structuredContent Against outputSchema
799+
*/
760800
test('should validate structuredContent against outputSchema', async () => {
761801
const server = new Server({
762802
name: 'test-server',
@@ -828,6 +868,9 @@ describe('outputSchema validation', () => {
828868
expect(result.structuredContent).toEqual({ result: 'success', count: 42 });
829869
});
830870

871+
/***
872+
* Test: Throw Error when structuredContent Does Not Match Schema
873+
*/
831874
test('should throw error when structuredContent does not match schema', async () => {
832875
const server = new Server({
833876
name: 'test-server',
@@ -901,6 +944,9 @@ describe('outputSchema validation', () => {
901944
);
902945
});
903946

947+
/***
948+
* Test: Throw Error when Tool with outputSchema Returns No structuredContent
949+
*/
904950
test('should throw error when tool with outputSchema returns no structuredContent', async () => {
905951
const server = new Server({
906952
name: 'test-server',
@@ -972,6 +1018,9 @@ describe('outputSchema validation', () => {
9721018
);
9731019
});
9741020

1021+
/***
1022+
* Test: Handle Tools Without outputSchema Normally
1023+
*/
9751024
test('should handle tools without outputSchema normally', async () => {
9761025
const server = new Server({
9771026
name: 'test-server',
@@ -1036,6 +1085,9 @@ describe('outputSchema validation', () => {
10361085
expect(result.content).toEqual([{ type: 'text', text: 'Normal response' }]);
10371086
});
10381087

1088+
/***
1089+
* Test: Handle Complex JSON Schema Validation
1090+
*/
10391091
test('should handle complex JSON schema validation', async () => {
10401092
const server = new Server({
10411093
name: 'test-server',
@@ -1131,6 +1183,9 @@ describe('outputSchema validation', () => {
11311183
expect(structuredContent.age).toBe(30);
11321184
});
11331185

1186+
/***
1187+
* Test: Fail Validation with Additional Properties When Not Allowed
1188+
*/
11341189
test('should fail validation with additional properties when not allowed', async () => {
11351190
const server = new Server({
11361191
name: 'test-server',
@@ -1206,6 +1261,9 @@ describe('outputSchema validation', () => {
12061261
);
12071262
});
12081263

1264+
/***
1265+
* Test: Throw Error when Tool Without outputSchema Returns structuredContent
1266+
*/
12091267
test('should throw error when tool without outputSchema returns structuredContent', async () => {
12101268
const server = new Server({
12111269
name: 'test-server',

src/client/index.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
McpError,
4545
} from "../types.js";
4646
import { z } from "zod";
47-
import { parseSchema } from "json-schema-to-zod";
47+
import { JsonSchema, parseSchema } from "json-schema-to-zod";
4848

4949
export type ClientOptions = ProtocolOptions & {
5050
/**
@@ -91,7 +91,6 @@ export class Client<
9191
private _serverVersion?: Implementation;
9292
private _capabilities: ClientCapabilities;
9393
private _instructions?: string;
94-
private _cachedTools: Map<string, Tool> = new Map();
9594
private _cachedToolOutputSchemas: Map<string, z.ZodTypeAny> = new Map();
9695

9796
/**
@@ -427,7 +426,7 @@ export class Client<
427426
);
428427

429428
// Check if the tool has an outputSchema
430-
const outputSchema = this._cachedToolOutputSchemas.get(params.name);
429+
const outputSchema = this.getToolOutputSchema(params.name);
431430
if (outputSchema) {
432431
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
433432
if (!result.structuredContent && !result.isError) {
@@ -472,27 +471,14 @@ export class Client<
472471
return result;
473472
}
474473

475-
async listTools(
476-
params?: ListToolsRequest["params"],
477-
options?: RequestOptions,
478-
) {
479-
const result = await this.request(
480-
{ method: "tools/list", params },
481-
ListToolsResultSchema,
482-
options,
483-
);
484-
485-
// Cache the tools and their output schemas for future validation
486-
this._cachedTools.clear();
474+
private cacheToolOutputSchemas(tools: Tool[]) {
487475
this._cachedToolOutputSchemas.clear();
488476

489-
for (const tool of result.tools) {
490-
this._cachedTools.set(tool.name, tool);
491-
477+
for (const tool of tools) {
492478
// If the tool has an outputSchema, create and cache the Zod schema
493479
if (tool.outputSchema) {
494480
try {
495-
const zodSchemaCode = parseSchema(tool.outputSchema);
481+
const zodSchemaCode = parseSchema(tool.outputSchema as JsonSchema);
496482
// The library returns a string of Zod code, we need to evaluate it
497483
// Using Function constructor to safely evaluate the Zod schema
498484
const createSchema = new Function('z', `return ${zodSchemaCode}`);
@@ -503,6 +489,24 @@ export class Client<
503489
}
504490
}
505491
}
492+
}
493+
494+
private getToolOutputSchema(toolName: string): z.ZodTypeAny | undefined {
495+
return this._cachedToolOutputSchemas.get(toolName);
496+
}
497+
498+
async listTools(
499+
params?: ListToolsRequest["params"],
500+
options?: RequestOptions,
501+
) {
502+
const result = await this.request(
503+
{ method: "tools/list", params },
504+
ListToolsResultSchema,
505+
options,
506+
);
507+
508+
// Cache the tools and their output schemas for future validation
509+
this.cacheToolOutputSchemas(result.tools);
506510

507511
return result;
508512
}

src/examples/server/mcpServerOutputSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ server.tool(
192192
}
193193
},
194194
required: ["stats"]
195-
},
195+
},
196196
async ({ data }) => {
197197
const mean = data.reduce((a, b) => a + b, 0) / data.length;
198198
const sorted = [...data].sort((a, b) => a - b);

0 commit comments

Comments
 (0)