- add new tool() api and tests

- fix schema validation
- comment headers on tests
This commit is contained in:
Basil Hosmer 2025-05-13 08:44:03 -04:00
commit 6079cd8307
No known key found for this signature in database
8 changed files with 578 additions and 401 deletions

10
package-lock.json generated
View file

@ -15,7 +15,6 @@
"eventsource": "^3.0.2",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"json-schema-to-zod": "^2.6.1",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
@ -4880,15 +4879,6 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema-to-zod": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.6.1.tgz",
"integrity": "sha512-uiHmWH21h9FjKJkRBntfVGTLpYlCZ1n98D0izIlByqQLqpmkQpNTBtfbdP04Na6+43lgsvrShFh2uWLkQDKJuQ==",
"license": "ISC",
"bin": {
"json-schema-to-zod": "dist/cjs/cli.js"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",

View file

@ -21,6 +21,9 @@ import { Transport } from "../shared/transport.js";
import { Server } from "../server/index.js";
import { InMemoryTransport } from "../inMemory.js";
/***
* Test: Initialize with Matching Protocol Version
*/
test("should initialize with matching protocol version", async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
@ -76,6 +79,9 @@ test("should initialize with matching protocol version", async () => {
expect(client.getInstructions()).toEqual("test instructions");
});
/***
* Test: Initialize with Supported Older Protocol Version
*/
test("should initialize with supported older protocol version", async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const clientTransport: Transport = {
@ -124,6 +130,9 @@ test("should initialize with supported older protocol version", async () => {
expect(client.getInstructions()).toBeUndefined();
});
/***
* Test: Reject Unsupported Protocol Version
*/
test("should reject unsupported protocol version", async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
@ -166,6 +175,9 @@ test("should reject unsupported protocol version", async () => {
expect(clientTransport.close).toHaveBeenCalled();
});
/***
* Test: Connect New Client to Old Supported Server Version
*/
test("should connect new client to old, supported server version", async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const server = new Server(
@ -229,6 +241,9 @@ test("should connect new client to old, supported server version", async () => {
});
});
/***
* Test: Version Negotiation with Old Client and Newer Server
*/
test("should negotiate version when client is old, and newer server supports its version", async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const server = new Server(
@ -292,6 +307,9 @@ test("should negotiate version when client is old, and newer server supports its
});
});
/***
* Test: Throw when Old Client and Server Version Mismatch
*/
test("should throw when client is old, and server doesn't support its version", async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const FUTURE_VERSION = "FUTURE_VERSION";
@ -354,6 +372,9 @@ test("should throw when client is old, and server doesn't support its version",
});
/***
* Test: Respect Server Capabilities
*/
test("should respect server capabilities", async () => {
const server = new Server(
{
@ -434,6 +455,9 @@ test("should respect server capabilities", async () => {
).rejects.toThrow("Server does not support completions");
});
/***
* Test: Respect Client Notification Capabilities
*/
test("should respect client notification capabilities", async () => {
const server = new Server(
{
@ -490,6 +514,9 @@ test("should respect client notification capabilities", async () => {
);
});
/***
* Test: Respect Server Notification Capabilities
*/
test("should respect server notification capabilities", async () => {
const server = new Server(
{
@ -536,6 +563,9 @@ test("should respect server notification capabilities", async () => {
);
});
/***
* Test: Only Allow setRequestHandler for Declared Capabilities
*/
test("should only allow setRequestHandler for declared capabilities", () => {
const client = new Client(
{
@ -567,9 +597,10 @@ test("should only allow setRequestHandler for declared capabilities", () => {
}).toThrow("Client does not support roots capability");
});
/*
Test that custom request/notification/result schemas can be used with the Client class.
*/
/***
* Test: Type Checking
* Test that custom request/notification/result schemas can be used with the Client class.
*/
test("should typecheck", () => {
const GetWeatherRequestSchema = RequestSchema.extend({
method: z.literal("weather/get"),
@ -646,6 +677,9 @@ test("should typecheck", () => {
});
});
/***
* Test: Handle Client Cancelling a Request
*/
test("should handle client cancelling a request", async () => {
const server = new Server(
{
@ -701,6 +735,9 @@ test("should handle client cancelling a request", async () => {
await expect(listResourcesPromise).rejects.toBe("Cancelled by test");
});
/***
* Test: Handle Request Timeout
*/
test("should handle request timeout", async () => {
const server = new Server(
{
@ -757,6 +794,9 @@ test("should handle request timeout", async () => {
});
describe('outputSchema validation', () => {
/***
* Test: Validate structuredContent Against outputSchema
*/
test('should validate structuredContent against outputSchema', async () => {
const server = new Server({
name: 'test-server',
@ -828,6 +868,9 @@ describe('outputSchema validation', () => {
expect(result.structuredContent).toEqual({ result: 'success', count: 42 });
});
/***
* Test: Throw Error when structuredContent Does Not Match Schema
*/
test('should throw error when structuredContent does not match schema', async () => {
const server = new Server({
name: 'test-server',
@ -901,6 +944,9 @@ describe('outputSchema validation', () => {
);
});
/***
* Test: Throw Error when Tool with outputSchema Returns No structuredContent
*/
test('should throw error when tool with outputSchema returns no structuredContent', async () => {
const server = new Server({
name: 'test-server',
@ -972,6 +1018,9 @@ describe('outputSchema validation', () => {
);
});
/***
* Test: Handle Tools Without outputSchema Normally
*/
test('should handle tools without outputSchema normally', async () => {
const server = new Server({
name: 'test-server',
@ -1036,6 +1085,9 @@ describe('outputSchema validation', () => {
expect(result.content).toEqual([{ type: 'text', text: 'Normal response' }]);
});
/***
* Test: Handle Complex JSON Schema Validation
*/
test('should handle complex JSON schema validation', async () => {
const server = new Server({
name: 'test-server',
@ -1131,6 +1183,9 @@ describe('outputSchema validation', () => {
expect(structuredContent.age).toBe(30);
});
/***
* Test: Fail Validation with Additional Properties When Not Allowed
*/
test('should fail validation with additional properties when not allowed', async () => {
const server = new Server({
name: 'test-server',
@ -1206,6 +1261,9 @@ describe('outputSchema validation', () => {
);
});
/***
* Test: Throw Error when Tool Without outputSchema Returns structuredContent
*/
test('should throw error when tool without outputSchema returns structuredContent', async () => {
const server = new Server({
name: 'test-server',

View file

@ -44,7 +44,7 @@ import {
McpError,
} from "../types.js";
import { z } from "zod";
import { parseSchema } from "json-schema-to-zod";
import { JsonSchema, parseSchema } from "json-schema-to-zod";
export type ClientOptions = ProtocolOptions & {
/**
@ -91,7 +91,6 @@ export class Client<
private _serverVersion?: Implementation;
private _capabilities: ClientCapabilities;
private _instructions?: string;
private _cachedTools: Map<string, Tool> = new Map();
private _cachedToolOutputSchemas: Map<string, z.ZodTypeAny> = new Map();
/**
@ -427,7 +426,7 @@ export class Client<
);
// Check if the tool has an outputSchema
const outputSchema = this._cachedToolOutputSchemas.get(params.name);
const outputSchema = this.getToolOutputSchema(params.name);
if (outputSchema) {
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
if (!result.structuredContent && !result.isError) {
@ -472,6 +471,30 @@ export class Client<
return result;
}
private cacheToolOutputSchemas(tools: Tool[]) {
this._cachedToolOutputSchemas.clear();
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the Zod schema
if (tool.outputSchema) {
try {
const zodSchemaCode = parseSchema(tool.outputSchema as JsonSchema);
// The library returns a string of Zod code, we need to evaluate it
// Using Function constructor to safely evaluate the Zod schema
const createSchema = new Function('z', `return ${zodSchemaCode}`);
const zodSchema = createSchema(z);
this._cachedToolOutputSchemas.set(tool.name, zodSchema);
} catch (error) {
console.warn(`Failed to create Zod schema for tool ${tool.name}: ${error}`);
}
}
}
}
private getToolOutputSchema(toolName: string): z.ZodTypeAny | undefined {
return this._cachedToolOutputSchemas.get(toolName);
}
async listTools(
params?: ListToolsRequest["params"],
options?: RequestOptions,
@ -483,26 +506,7 @@ export class Client<
);
// Cache the tools and their output schemas for future validation
this._cachedTools.clear();
this._cachedToolOutputSchemas.clear();
for (const tool of result.tools) {
this._cachedTools.set(tool.name, tool);
// If the tool has an outputSchema, create and cache the Zod schema
if (tool.outputSchema) {
try {
const zodSchemaCode = parseSchema(tool.outputSchema);
// The library returns a string of Zod code, we need to evaluate it
// Using Function constructor to safely evaluate the Zod schema
const createSchema = new Function('z', `return ${zodSchemaCode}`);
const zodSchema = createSchema(z);
this._cachedToolOutputSchemas.set(tool.name, zodSchema);
} catch (error) {
console.warn(`Failed to create Zod schema for tool ${tool.name}: ${error}`);
}
}
}
this.cacheToolOutputSchemas(result.tools);
return result;
}

View file

@ -192,7 +192,7 @@ server.tool(
}
},
required: ["stats"]
},
},
async ({ data }) => {
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const sorted = [...data].sort((a, b) => a - b);

View file

@ -1,222 +0,0 @@
import { McpServer } from './mcp.js';
import { Client } from '../client/index.js';
import { InMemoryTransport } from '../inMemory.js';
import { z } from 'zod';
describe('McpServer outputSchema support', () => {
let server: McpServer;
let client: Client;
let serverTransport: InMemoryTransport;
let clientTransport: InMemoryTransport;
beforeEach(async () => {
server = new McpServer({ name: 'test', version: '1.0' });
client = new Client({ name: 'test-client', version: '1.0' });
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
});
afterEach(async () => {
await client.close();
await server.close();
});
describe('tool registration with outputSchema', () => {
it('should register a tool with outputSchema', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' },
count: { type: 'number' }
},
required: ['result', 'count']
};
const tool = server.tool(
'test-tool',
'A test tool',
{ input: z.string() },
outputSchema,
() => ({ structuredContent: { result: 'test', count: 42 } })
);
expect(tool.outputSchema).toEqual(outputSchema);
// Connect after registering the tool
await server.connect(serverTransport);
await client.connect(clientTransport);
});
it('should include outputSchema in ListToolsResult', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' }
}
};
server.tool(
'structured-tool',
{ input: z.string() },
outputSchema,
() => ({ structuredContent: { result: 'test' } })
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.listTools();
expect(result.tools[0].outputSchema).toEqual(outputSchema);
});
});
describe('tool execution with outputSchema', () => {
it('should return structuredContent and auto-generate content for backward compatibility', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' },
count: { type: 'number' }
}
};
server.tool(
'structured-tool',
{ input: z.string() },
outputSchema,
() => ({
structuredContent: { result: 'test', count: 42 }
})
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
// Need to call listTools first to cache the outputSchema
await client.listTools();
const result = await client.callTool({ name: 'structured-tool', arguments: { input: 'test' } });
expect(result.structuredContent).toEqual({ result: 'test', count: 42 });
expect(result.content).toEqual([{
type: 'text',
text: JSON.stringify({ result: 'test', count: 42 }, null, 2)
}]);
});
it('should preserve both content and structuredContent if tool provides both', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' }
}
};
server.tool(
'structured-tool',
{ input: z.string() },
outputSchema,
() => ({
structuredContent: { result: 'test' },
content: [{ type: 'text', text: 'Custom text' }]
})
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
// Need to call listTools first to cache the outputSchema
await client.listTools();
const result = await client.callTool({ name: 'structured-tool', arguments: { input: 'test' } });
expect(result.structuredContent).toEqual({ result: 'test' });
expect(result.content).toEqual([{ type: 'text', text: 'Custom text' }]);
});
it('should throw error if tool with outputSchema returns no structuredContent', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' }
}
};
server.tool(
'broken-tool',
{ input: z.string() },
outputSchema,
() => ({
content: [{ type: 'text', text: 'No structured content' }]
})
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
// Need to call listTools first to cache the outputSchema
await client.listTools();
await expect(client.callTool({ name: 'broken-tool', arguments: { input: 'test' } }))
.rejects.toThrow('has outputSchema but returned no structuredContent');
});
it('should throw error if tool without outputSchema returns structuredContent', async () => {
server.tool(
'broken-tool',
{ input: z.string() },
() => ({
structuredContent: { result: 'test' }
})
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
// Call listTools first (but in this case the tool has no outputSchema)
await client.listTools();
await expect(client.callTool({ name: 'broken-tool', arguments: { input: 'test' } }))
.rejects.toThrow('has no outputSchema but returned structuredContent');
});
it('should handle error results properly for tools with outputSchema', async () => {
const outputSchema = {
type: 'object',
properties: {
result: { type: 'string' }
}
};
server.tool(
'error-tool',
{ input: z.string() },
outputSchema,
() => {
throw new Error('Tool error');
}
);
// Now connect
await server.connect(serverTransport);
await client.connect(clientTransport);
// Need to call listTools first to cache the outputSchema
await client.listTools();
const result = await client.callTool({ name: 'error-tool', arguments: { input: 'test' } });
expect(result.isError).toBe(true);
expect(result.content).toEqual([{
type: 'text',
text: 'Tool error'
}]);
expect(result.structuredContent).toBeUndefined();
});
});
});

View file

@ -13,12 +13,16 @@ import {
CompleteResultSchema,
LoggingMessageNotificationSchema,
Notification,
TextContent,
} from "../types.js";
import { ResourceTemplate } from "./mcp.js";
import { completable } from "./completable.js";
import { UriTemplate } from "../shared/uriTemplate.js";
describe("McpServer", () => {
/***
* Test: Basic Server Instance
*/
test("should expose underlying Server instance", () => {
const mcpServer = new McpServer({
name: "test server",
@ -28,6 +32,9 @@ describe("McpServer", () => {
expect(mcpServer.server).toBeDefined();
});
/***
* Test: Notification Sending via Server
*/
test("should allow sending notifications via Server", async () => {
const mcpServer = new McpServer(
{
@ -75,6 +82,9 @@ describe("McpServer", () => {
});
describe("ResourceTemplate", () => {
/***
* Test: ResourceTemplate Creation with String Pattern
*/
test("should create ResourceTemplate with string pattern", () => {
const template = new ResourceTemplate("test://{category}/{id}", {
list: undefined,
@ -83,6 +93,9 @@ describe("ResourceTemplate", () => {
expect(template.listCallback).toBeUndefined();
});
/***
* Test: ResourceTemplate Creation with UriTemplate Instance
*/
test("should create ResourceTemplate with UriTemplate", () => {
const uriTemplate = new UriTemplate("test://{category}/{id}");
const template = new ResourceTemplate(uriTemplate, { list: undefined });
@ -90,6 +103,9 @@ describe("ResourceTemplate", () => {
expect(template.listCallback).toBeUndefined();
});
/***
* Test: ResourceTemplate with List Callback
*/
test("should create ResourceTemplate with list callback", async () => {
const list = jest.fn().mockResolvedValue({
resources: [{ name: "Test", uri: "test://example" }],
@ -111,6 +127,9 @@ describe("ResourceTemplate", () => {
});
describe("tool()", () => {
/***
* Test: Zero-Argument Tool Registration
*/
test("should register zero-argument tool", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -178,6 +197,9 @@ describe("tool()", () => {
])
});
/***
* Test: Updating Existing Tool
*/
test("should update existing tool", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -244,6 +266,9 @@ describe("tool()", () => {
expect(notifications).toHaveLength(0)
});
/***
* Test: Updating Tool with Schema
*/
test("should update tool with schema", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -339,6 +364,9 @@ describe("tool()", () => {
expect(notifications).toHaveLength(0)
});
/***
* Test: Tool List Changed Notifications
*/
test("should send tool list changed notifications when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -404,6 +432,9 @@ describe("tool()", () => {
])
});
/***
* Test: Tool Registration with Parameters
*/
test("should register tool with params", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -414,6 +445,7 @@ describe("tool()", () => {
version: "1.0",
});
// old api
mcpServer.tool(
"test",
{
@ -430,6 +462,17 @@ describe("tool()", () => {
}),
);
// new api
mcpServer.tool(
"test (new api)",
{
inputSchema: { name: z.string(), value: z.number() },
callback: async ({ name, value }) => ({
content: [{ type: "text", text: `${name}: ${value}` }],
}),
}
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -445,7 +488,7 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].inputSchema).toMatchObject({
type: "object",
@ -454,8 +497,13 @@ describe("tool()", () => {
value: { type: "number" },
},
});
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema);
});
/***
* Test: Tool Registration with Description
*/
test("should register tool with description", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -466,6 +514,7 @@ describe("tool()", () => {
version: "1.0",
});
// old api
mcpServer.tool("test", "Test description", async () => ({
content: [
{
@ -475,6 +524,20 @@ describe("tool()", () => {
],
}));
// new api
mcpServer.tool("test (new api)", {
description: "Test description",
callback: async ({}) => ({
content: [
{
type: "text",
text: "Test response",
},
],
})
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -490,11 +553,16 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].description).toBe("Test description");
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].description).toBe("Test description");
});
/***
* Test: Tool Registration with Annotations
*/
test("should register tool with annotations", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -514,6 +582,18 @@ describe("tool()", () => {
],
}));
mcpServer.tool("test (new api)", {
annotations: { title: "Test Tool", readOnlyHint: true },
callback: async ({}) => ({
content: [
{
type: "text",
text: "Test response",
},
],
}),
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -529,11 +609,16 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true });
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].annotations).toEqual({ title: "Test Tool", readOnlyHint: true });
});
/***
* Test: Tool Registration with Parameters and Annotations
*/
test("should register tool with params and annotations", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -553,6 +638,14 @@ describe("tool()", () => {
})
);
mcpServer.tool("test (new api)", {
inputSchema: { name: z.string() },
annotations: { title: "Test Tool", readOnlyHint: true },
callback: async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }]
})
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -566,15 +659,21 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].inputSchema).toMatchObject({
type: "object",
properties: { name: { type: "string" } }
});
expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true });
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema);
expect(result.tools[1].annotations).toEqual(result.tools[0].annotations);
});
/***
* Test: Tool Registration with Description, Parameters, and Annotations
*/
test("should register tool with description, params, and annotations", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -595,6 +694,15 @@ describe("tool()", () => {
})
);
mcpServer.tool("test (new api)", {
description: "A tool with everything",
inputSchema: { name: z.string() },
annotations: { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false },
callback: async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }]
})
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -608,7 +716,7 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].description).toBe("A tool with everything");
expect(result.tools[0].inputSchema).toMatchObject({
@ -620,8 +728,15 @@ describe("tool()", () => {
readOnlyHint: true,
openWorldHint: false
});
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].description).toBe("A tool with everything");
expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema);
expect(result.tools[1].annotations).toEqual(result.tools[0].annotations);
});
/***
* Test: Tool Registration with Description, Empty Parameters, and Annotations
*/
test("should register tool with description, empty params, and annotations", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -642,6 +757,15 @@ describe("tool()", () => {
})
);
mcpServer.tool("test (new api)", {
description: "A tool with everything but empty params",
inputSchema: {},
annotations: { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false },
callback: async ({}) => ({
content: [{ type: "text", text: "Test response" }]
})
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -655,7 +779,7 @@ describe("tool()", () => {
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("test");
expect(result.tools[0].description).toBe("A tool with everything but empty params");
expect(result.tools[0].inputSchema).toMatchObject({
@ -667,8 +791,15 @@ describe("tool()", () => {
readOnlyHint: true,
openWorldHint: false
});
expect(result.tools[1].name).toBe("test (new api)");
expect(result.tools[1].description).toBe("A tool with everything but empty params");
expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema);
expect(result.tools[1].annotations).toEqual(result.tools[0].annotations);
});
/***
* Test: Tool Argument Validation
*/
test("should validate tool args", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -703,6 +834,21 @@ describe("tool()", () => {
}),
);
mcpServer.tool("test (new api)", {
inputSchema: {
name: z.string(),
value: z.number(),
},
callback: async ({ name, value }) => ({
content: [
{
type: "text",
text: `${name}: ${value}`,
},
],
}),
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -726,8 +872,27 @@ describe("tool()", () => {
CallToolResultSchema,
),
).rejects.toThrow(/Invalid arguments/);
await expect(
client.request(
{
method: "tools/call",
params: {
name: "test (new api)",
arguments: {
name: "test",
value: "not a number",
},
},
},
CallToolResultSchema,
),
).rejects.toThrow(/Invalid arguments/);
});
/***
* Test: Preventing Duplicate Tool Registration
*/
test("should prevent duplicate tool registration", () => {
const mcpServer = new McpServer({
name: "test server",
@ -755,6 +920,9 @@ describe("tool()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Multiple Tool Registration
*/
test("should allow registering multiple tools", () => {
const mcpServer = new McpServer({
name: "test server",
@ -768,6 +936,9 @@ describe("tool()", () => {
mcpServer.tool("tool2", () => ({ content: [] }));
});
/***
* Test: Tool with Output Schema and Structured Content
*/
test("should support tool with outputSchema and structuredContent", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -787,35 +958,28 @@ describe("tool()", () => {
);
// Register a tool with outputSchema
const registeredTool = mcpServer.tool(
mcpServer.tool(
"test",
"Test tool with structured output",
{
input: z.string(),
description: "Test tool with structured output",
inputSchema: {
input: z.string(),
},
outputSchema: {
processedInput: z.string(),
resultType: z.string(),
timestamp: z.string()
},
callback: async ({ input }) => ({
structuredContent: {
processedInput: input,
resultType: "structured",
timestamp: "2023-01-01T00:00:00Z"
},
}),
},
async ({ input }) => ({
// When outputSchema is defined, return structuredContent instead of content
structuredContent: {
processedInput: input,
resultType: "structured",
timestamp: "2023-01-01T00:00:00Z"
},
}),
);
// Update the tool to add outputSchema
registeredTool.update({
outputSchema: {
type: "object",
properties: {
processedInput: { type: "string" },
resultType: { type: "string" },
timestamp: { type: "string", format: "date-time" }
},
required: ["processedInput", "resultType", "timestamp"]
}
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -833,17 +997,17 @@ describe("tool()", () => {
);
expect(listResult.tools).toHaveLength(1);
expect(listResult.tools[0].outputSchema).toEqual({
expect(listResult.tools[0].outputSchema).toMatchObject({
type: "object",
properties: {
processedInput: { type: "string" },
resultType: { type: "string" },
timestamp: { type: "string", format: "date-time" }
timestamp: { type: "string" }
},
required: ["processedInput", "resultType", "timestamp"]
});
// Call the tool and verify it returns structuredContent
// Call the tool and verify it returns valid structuredContent
const result = await client.request(
{
method: "tools/call",
@ -858,9 +1022,6 @@ describe("tool()", () => {
);
expect(result.structuredContent).toBeDefined();
// For backward compatibility, content is auto-generated from structuredContent
expect(result.content).toBeDefined();
const structuredContent = result.structuredContent as {
processedInput: string;
resultType: string;
@ -869,8 +1030,85 @@ describe("tool()", () => {
expect(structuredContent.processedInput).toBe("hello");
expect(structuredContent.resultType).toBe("structured");
expect(structuredContent.timestamp).toBe("2023-01-01T00:00:00Z");
// For backward compatibility, content is auto-generated from structuredContent
expect(result.content).toBeDefined();
expect(result.content!).toHaveLength(1);
expect(result.content![0]).toMatchObject({ type: "text" });
const textContent = result.content![0] as TextContent;
expect(JSON.parse(textContent.text)).toEqual(result.structuredContent);
});
/***
* Test: Schema Validation Failure for Invalid Structured Content
*/
test("should fail schema validation when tool returns invalid structuredContent", async () => {
const mcpServer = new McpServer({
name: "test server",
version: "1.0",
});
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {
tools: {},
},
},
);
// Register a tool with outputSchema that returns invalid data
mcpServer.tool(
"test",
{
description: "Test tool with invalid structured output",
inputSchema: {
input: z.string(),
},
outputSchema: {
processedInput: z.string(),
resultType: z.string(),
timestamp: z.string()
},
callback: async ({ input }) => ({
structuredContent: {
processedInput: input,
resultType: "structured",
// Missing required 'timestamp' field
someExtraField: "unexpected" // Extra field not in schema
},
}),
},
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// First call listTools to cache the outputSchema in the client
await client.listTools();
// Call the tool and expect it to throw a validation error
await expect(
client.callTool({
name: "test",
arguments: {
input: "hello",
},
}),
).rejects.toThrow(/Structured content does not match the tool's output schema/);
});
/***
* Test: Pass Session ID to Tool Callback
*/
test("should pass sessionId to tool callback via RequestHandlerExtra", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -924,6 +1162,9 @@ describe("tool()", () => {
expect(receivedSessionId).toBe("test-session-123");
});
/***
* Test: Pass Request ID to Tool Callback
*/
test("should pass requestId to tool callback via RequestHandlerExtra", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -977,6 +1218,9 @@ describe("tool()", () => {
expect(result.content && result.content[0].text).toContain("Received request ID:");
});
/***
* Test: Send Notification within Tool Call
*/
test("should provide sendNotification within tool call", async () => {
const mcpServer = new McpServer(
{
@ -1034,6 +1278,9 @@ describe("tool()", () => {
expect(receivedLogMessage).toBe(loggingMessage);
});
/***
* Test: Client to Server Tool Call
*/
test("should allow client to call server tools", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1097,6 +1344,9 @@ describe("tool()", () => {
]);
});
/***
* Test: Graceful Tool Error Handling
*/
test("should handle server tool errors gracefully", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1146,6 +1396,9 @@ describe("tool()", () => {
]);
});
/***
* Test: McpError for Invalid Tool Name
*/
test("should throw McpError for invalid tool name", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1196,6 +1449,9 @@ describe("tool()", () => {
});
describe("resource()", () => {
/***
* Test: Resource Registration with URI and Read Callback
*/
test("should register resource with uri and readCallback", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1235,6 +1491,9 @@ describe("resource()", () => {
expect(result.resources[0].uri).toBe("test://resource");
});
/***
* Test: Update Resource with URI
*/
test("should update resource with uri", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1297,6 +1556,9 @@ describe("resource()", () => {
expect(notifications).toHaveLength(0);
});
/***
* Test: Update Resource Template
*/
test("should update resource template", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1363,6 +1625,9 @@ describe("resource()", () => {
expect(notifications).toHaveLength(0);
});
/***
* Test: Resource List Changed Notification
*/
test("should send resource list changed notification when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1416,6 +1681,9 @@ describe("resource()", () => {
]);
});
/***
* Test: Remove Resource and Send Notification
*/
test("should remove resource and send notification when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1477,6 +1745,9 @@ describe("resource()", () => {
expect(result.resources[0].uri).toBe("test://resource2");
});
/***
* Test: Remove Resource Template and Send Notification
*/
test("should remove resource template and send notification when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1541,6 +1812,9 @@ describe("resource()", () => {
expect(result2.resourceTemplates).toHaveLength(0);
});
/***
* Test: Resource Registration with Metadata
*/
test("should register resource with metadata", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1588,6 +1862,9 @@ describe("resource()", () => {
expect(result.resources[0].mimeType).toBe("text/plain");
});
/***
* Test: Resource Template Registration
*/
test("should register resource template", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1633,6 +1910,9 @@ describe("resource()", () => {
);
});
/***
* Test: Resource Template with List Callback
*/
test("should register resource template with listCallback", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1691,6 +1971,9 @@ describe("resource()", () => {
expect(result.resources[1].uri).toBe("test://resource/2");
});
/***
* Test: Template Variables to Read Callback
*/
test("should pass template variables to readCallback", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1737,6 +2020,9 @@ describe("resource()", () => {
expect(result.contents[0].text).toBe("Category: books, ID: 123");
});
/***
* Test: Preventing Duplicate Resource Registration
*/
test("should prevent duplicate resource registration", () => {
const mcpServer = new McpServer({
name: "test server",
@ -1764,6 +2050,9 @@ describe("resource()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Multiple Resource Registration
*/
test("should allow registering multiple resources", () => {
const mcpServer = new McpServer({
name: "test server",
@ -1791,6 +2080,9 @@ describe("resource()", () => {
}));
});
/***
* Test: Preventing Duplicate Resource Template Registration
*/
test("should prevent duplicate resource template registration", () => {
const mcpServer = new McpServer({
name: "test server",
@ -1826,6 +2118,9 @@ describe("resource()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Graceful Resource Read Error Handling
*/
test("should handle resource read errors gracefully", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1861,6 +2156,9 @@ describe("resource()", () => {
).rejects.toThrow(/Resource read failed/);
});
/***
* Test: McpError for Invalid Resource URI
*/
test("should throw McpError for invalid resource URI", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1901,6 +2199,9 @@ describe("resource()", () => {
).rejects.toThrow(/Resource test:\/\/nonexistent not found/);
});
/***
* Test: Resource Template Parameter Completion
*/
test("should support completion of resource template parameters", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1966,6 +2267,9 @@ describe("resource()", () => {
expect(result.completion.total).toBe(3);
});
/***
* Test: Filtered Resource Template Parameter Completion
*/
test("should support filtered completion of resource template parameters", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2034,6 +2338,9 @@ describe("resource()", () => {
expect(result.completion.total).toBe(2);
});
/***
* Test: Pass Request ID to Resource Callback
*/
test("should pass requestId to resource callback via RequestHandlerExtra", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2089,6 +2396,9 @@ describe("resource()", () => {
});
describe("prompt()", () => {
/***
* Test: Zero-Argument Prompt Registration
*/
test("should register zero-argument prompt", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2130,6 +2440,9 @@ describe("prompt()", () => {
expect(result.prompts[0].name).toBe("test");
expect(result.prompts[0].arguments).toBeUndefined();
});
/***
* Test: Updating Existing Prompt
*/
test("should update existing prompt", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2198,6 +2511,9 @@ describe("prompt()", () => {
expect(notifications).toHaveLength(0);
});
/***
* Test: Updating Prompt with Schema
*/
test("should update prompt with schema", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2291,6 +2607,9 @@ describe("prompt()", () => {
expect(notifications).toHaveLength(0);
});
/***
* Test: Prompt List Changed Notification
*/
test("should send prompt list changed notification when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2350,6 +2669,9 @@ describe("prompt()", () => {
]);
});
/***
* Test: Remove Prompt and Send Notification
*/
test("should remove prompt and send notification when connected", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2428,6 +2750,9 @@ describe("prompt()", () => {
expect(result.prompts[0].name).toBe("prompt2");
});
/***
* Test: Prompt Registration with Arguments Schema
*/
test("should register prompt with args schema", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2480,6 +2805,9 @@ describe("prompt()", () => {
]);
});
/***
* Test: Prompt Registration with Description
*/
test("should register prompt with description", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2522,6 +2850,9 @@ describe("prompt()", () => {
expect(result.prompts[0].description).toBe("Test description");
});
/***
* Test: Prompt Argument Validation
*/
test("should validate prompt args", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2584,6 +2915,9 @@ describe("prompt()", () => {
).rejects.toThrow(/Invalid arguments/);
});
/***
* Test: Preventing Duplicate Prompt Registration
*/
test("should prevent duplicate prompt registration", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2617,6 +2951,9 @@ describe("prompt()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Multiple Prompt Registration
*/
test("should allow registering multiple prompts", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2650,6 +2987,9 @@ describe("prompt()", () => {
}));
});
/***
* Test: Prompt Registration with Arguments
*/
test("should allow registering prompts with arguments", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2672,6 +3012,9 @@ describe("prompt()", () => {
);
});
/***
* Test: Resources and Prompts with Completion Handlers
*/
test("should allow registering both resources and prompts with completion handlers", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2713,6 +3056,9 @@ describe("prompt()", () => {
);
});
/***
* Test: McpError for Invalid Prompt Name
*/
test("should throw McpError for invalid prompt name", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2764,6 +3110,9 @@ describe("prompt()", () => {
).rejects.toThrow(/Prompt nonexistent-prompt not found/);
});
/***
* Test: Prompt Argument Completion
*/
test("should support completion of prompt arguments", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2829,6 +3178,9 @@ describe("prompt()", () => {
expect(result.completion.total).toBe(3);
});
/***
* Test: Filtered Prompt Argument Completion
*/
test("should support filtered completion of prompt arguments", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2896,6 +3248,9 @@ describe("prompt()", () => {
expect(result.completion.total).toBe(1);
});
/***
* Test: Pass Request ID to Prompt Callback
*/
test("should pass requestId to prompt callback via RequestHandlerExtra", async () => {
const mcpServer = new McpServer({
name: "test server",

View file

@ -90,7 +90,7 @@ export class McpServer {
if (this._toolHandlersInitialized) {
return;
}
this.server.assertCanSetRequestHandler(
ListToolsRequestSchema.shape.method.value,
);
@ -116,15 +116,17 @@ export class McpServer {
description: tool.description,
inputSchema: tool.inputSchema
? (zodToJsonSchema(tool.inputSchema, {
strictUnions: true,
}) as Tool["inputSchema"])
strictUnions: true,
}) as Tool["inputSchema"])
: EMPTY_OBJECT_JSON_SCHEMA,
annotations: tool.annotations,
};
// Only include outputSchema if it's defined
if (tool.outputSchema) {
toolDefinition.outputSchema = tool.outputSchema;
toolDefinition.outputSchema = zodToJsonSchema(
tool.outputSchema,
{ strictUnions: true }
) as Tool["outputSchema"];
}
return toolDefinition;
@ -447,7 +449,7 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._resourceHandlersInitialized = true;
}
@ -530,7 +532,7 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._promptHandlersInitialized = true;
}
@ -682,7 +684,7 @@ export class McpServer {
paramsSchemaOrAnnotations: Args | ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with both parameter schema and annotations.
*/
@ -692,7 +694,7 @@ export class McpServer {
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with description, parameter schema, and annotations.
*/
@ -701,117 +703,92 @@ export class McpServer {
description: string,
paramsSchema: Args,
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
callback: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with output schema.
* Registers a tool from a config object.
*/
tool<Args extends ZodRawShape>(
tool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
name: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
cb: ToolCallback<Args>,
config: {
description?: string;
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
callback: ToolCallback<InputArgs>
}
): RegisteredTool;
/**
* Registers a tool with description and output schema.
* tool() implementation. Parses arguments passed to overrides defined above.
*/
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with parameter schema, output schema, and annotations.
*/
tool<Args extends ZodRawShape>(
name: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with description, parameter schema, output schema, and annotations.
*/
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
tool(name: string, ...rest: unknown[]): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
}
let description: string | undefined;
if (typeof rest[0] === "string") {
description = rest.shift() as string;
}
let paramsSchema: ZodRawShape | undefined;
let outputSchema: Tool["outputSchema"] | undefined;
let inputSchema: ZodRawShape | undefined;
let outputSchema: ZodRawShape | undefined;
let annotations: ToolAnnotations | undefined;
// Handle the different overload combinations
if (rest.length > 1) {
// We have at least two more args before the callback
const firstArg = rest[0];
if (isZodRawShape(firstArg)) {
// We have a params schema as the first arg
paramsSchema = rest.shift() as ZodRawShape;
// Check if the next arg is potentially annotations or outputSchema
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null) {
const nextArg = rest[0];
let callback: ToolCallback<ZodRawShape | undefined>;
// Check if it's a JSON Schema (outputSchema)
if (typeof nextArg === "object" && "type" in nextArg) {
outputSchema = rest.shift() as Tool["outputSchema"];
if (rest.length == 1 && typeof rest[0] === "object" && rest[0] !== null && 'callback' in rest[0]) {
// New style: tool(name, { description, inputSchema, outputSchema, annotations, callback })
// Future additions to tool definition should be added to config object.
// Check if there's still an annotations object
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
annotations = rest.shift() as ToolAnnotations;
}
} else if (!(isZodRawShape(nextArg))) {
// It's annotations
annotations = rest.shift() as ToolAnnotations;
}
}
} else if (typeof firstArg === "object" && firstArg !== null) {
// Check if it's a JSON Schema (outputSchema)
if ("type" in firstArg) {
outputSchema = rest.shift() as Tool["outputSchema"];
// Check if there's still an annotations object
const config = rest[0] as {
description?: string;
inputSchema?: ZodRawShape;
outputSchema?: ZodRawShape;
annotations?: ToolAnnotations;
callback: ToolCallback<ZodRawShape>;
};
({ description, inputSchema, outputSchema, annotations, callback } = config) ;
} else {
// Old style: tool properties are passed as separate arguments, with omissions allowed.
// Support for this style is frozen as of protocol version 2025-03-26. Future additions
// to tool definition should *NOT* be added.
if (typeof rest[0] === "string") {
description = rest.shift() as string;
}
// Handle the different overload combinations
if (rest.length > 1) {
// We have at least one more arg before the callback
const firstArg = rest[0];
if (isZodRawShape(firstArg)) {
// We have a params schema as the first arg
inputSchema = rest.shift() as ZodRawShape;
// Check if the next arg is potentially annotations
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
// Case: tool(name, paramsSchema, annotations, cb)
// Or: tool(name, description, paramsSchema, annotations, cb)
annotations = rest.shift() as ToolAnnotations;
}
} else {
// It's annotations
} else if (typeof firstArg === "object" && firstArg !== null) {
// Not a ZodRawShape, so must be annotations in this position
// Case: tool(name, annotations, cb)
// Or: tool(name, description, annotations, cb)
annotations = rest.shift() as ToolAnnotations;
}
}
callback = rest[0] as ToolCallback<ZodRawShape | undefined>;
}
const cb = rest[0] as ToolCallback<ZodRawShape | undefined>;
const registeredTool: RegisteredTool = {
description,
inputSchema:
paramsSchema === undefined ? undefined : z.object(paramsSchema),
outputSchema,
inputSchema === undefined ? undefined : z.object(inputSchema),
outputSchema:
outputSchema === undefined ? undefined : z.object(outputSchema),
annotations,
callback: cb,
callback: callback,
enabled: true,
disable: () => registeredTool.update({ enabled: false }),
enable: () => registeredTool.update({ enabled: true }),
@ -823,7 +800,6 @@ export class McpServer {
}
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = updates.outputSchema
if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback
if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations
if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled
@ -1019,22 +995,31 @@ export class ResourceTemplate {
*/
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
Args extends ZodRawShape
? (
args: z.objectOutputType<Args, ZodTypeAny>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
? (
args: z.objectOutputType<Args, ZodTypeAny>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
export type RegisteredTool = {
description?: string;
inputSchema?: AnyZodObject;
outputSchema?: Tool["outputSchema"];
outputSchema?: AnyZodObject;
annotations?: ToolAnnotations;
callback: ToolCallback<undefined | ZodRawShape>;
enabled: boolean;
enable(): void;
disable(): void;
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, outputSchema?: Tool["outputSchema"], callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
update<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
updates: {
name?: string | null,
description?: string,
paramsSchema?: InputArgs,
outputSchema?: OutputArgs,
annotations?: ToolAnnotations,
callback?: ToolCallback<InputArgs>,
enabled?: boolean
}): void
remove(): void
};
@ -1107,23 +1092,23 @@ export type RegisteredResourceTemplate = {
enabled: boolean;
enable(): void;
disable(): void;
update(updates: { name?: string | null, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
update(updates: { name?: string | null, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
remove(): void
};
type PromptArgsRawShape = {
[k: string]:
| ZodType<string, ZodTypeDef, string>
| ZodOptional<ZodType<string, ZodTypeDef, string>>;
| ZodType<string, ZodTypeDef, string>
| ZodOptional<ZodType<string, ZodTypeDef, string>>;
};
export type PromptCallback<
Args extends undefined | PromptArgsRawShape = undefined,
> = Args extends PromptArgsRawShape
? (
args: z.objectOutputType<Args, ZodTypeAny>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => GetPromptResult | Promise<GetPromptResult>
args: z.objectOutputType<Args, ZodTypeAny>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => GetPromptResult | Promise<GetPromptResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
export type RegisteredPrompt = {

View file

@ -838,7 +838,14 @@ export const ToolSchema = z
* If set, a CallToolResult for this Tool MUST contain a structuredContent field whose contents validate against this schema.
* If not set, a CallToolResult for this Tool MUST NOT contain a structuredContent field and MUST contain a content field.
*/
outputSchema: z.optional(z.object({}).passthrough()),
outputSchema: z.optional(
z.object({
type: z.literal("object"),
properties: z.optional(z.object({}).passthrough()),
required: z.optional(z.array(z.string())),
})
.passthrough()
),
/**
* Optional additional tool information.
*/