Merge pull request #454 from modelcontextprotocol/basil/output_schema

Prototype support for `outputSchema`/`structuredContent`
This commit is contained in:
bhosmer-ant 2025-05-15 10:41:37 -04:00 • committed by GitHub
commit 1fd6265751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1706 additions and 97 deletions

103
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.11.3",
"license": "MIT",
"dependencies": {
"ajv": "^8.17.1",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
@ -1035,6 +1036,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
@ -2198,15 +2223,15 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@ -3216,6 +3241,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/espree": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
@ -3450,8 +3499,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
@ -3500,6 +3548,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@ -4880,10 +4944,10 @@
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@ -5558,6 +5622,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@ -5662,6 +5727,15 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -6445,6 +6519,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}

View file

@ -46,6 +46,7 @@
"client": "tsx src/cli.ts client"
},
"dependencies": {
"ajv": "^8.17.1",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",

View file

@ -12,6 +12,7 @@ import {
InitializeRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
CreateMessageRequestSchema,
ListRootsRequestSchema,
ErrorCode,
@ -20,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),
@ -75,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 = {
@ -123,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),
@ -165,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(
@ -228,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(
@ -291,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";
@ -353,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(
{
@ -433,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(
{
@ -489,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(
{
@ -535,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(
{
@ -566,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"),
@ -645,6 +677,9 @@ test("should typecheck", () => {
});
});
/***
* Test: Handle Client Cancelling a Request
*/
test("should handle client cancelling a request", async () => {
const server = new Server(
{
@ -700,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(
{
@ -754,3 +792,474 @@ test("should handle request timeout", async () => {
code: ErrorCode.RequestTimeout,
});
});
describe('outputSchema validation', () => {
/***
* Test: Validate structuredContent Against outputSchema
*/
test('should validate structuredContent against outputSchema', async () => {
const server = new Server({
name: 'test-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {},
},
outputSchema: {
type: 'object',
properties: {
result: { type: 'string' },
count: { type: 'number' },
},
required: ['result', 'count'],
additionalProperties: false,
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
return {
structuredContent: { result: 'success', count: 42 },
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should validate successfully
const result = await client.callTool({ name: 'test-tool' });
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',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {},
},
outputSchema: {
type: 'object',
properties: {
result: { type: 'string' },
count: { type: 'number' },
},
required: ['result', 'count'],
additionalProperties: false,
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
// Return invalid structured content (count is string instead of number)
return {
structuredContent: { result: 'success', count: 'not a number' },
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should throw validation error
await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow(
/Structured content does not match the tool's output schema/
);
});
/***
* 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',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {},
},
outputSchema: {
type: 'object',
properties: {
result: { type: 'string' },
},
required: ['result'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
// Return content instead of structuredContent
return {
content: [{ type: 'text', text: 'This should be structured content' }],
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should throw error
await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow(
/Tool test-tool has an output schema but did not return structured content/
);
});
/***
* Test: Handle Tools Without outputSchema Normally
*/
test('should handle tools without outputSchema normally', async () => {
const server = new Server({
name: 'test-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {},
},
// No outputSchema
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
// Return regular content
return {
content: [{ type: 'text', text: 'Normal response' }],
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should work normally without validation
const result = await client.callTool({ name: 'test-tool' });
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',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'complex-tool',
description: 'A tool with complex schema',
inputSchema: {
type: 'object',
properties: {},
},
outputSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 3 },
age: { type: 'integer', minimum: 0, maximum: 120 },
active: { type: 'boolean' },
tags: {
type: 'array',
items: { type: 'string' },
minItems: 1,
},
metadata: {
type: 'object',
properties: {
created: { type: 'string' },
},
required: ['created'],
},
},
required: ['name', 'age', 'active', 'tags', 'metadata'],
additionalProperties: false,
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'complex-tool') {
return {
structuredContent: {
name: 'John Doe',
age: 30,
active: true,
tags: ['user', 'admin'],
metadata: {
created: '2023-01-01T00:00:00Z',
},
},
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should validate successfully
const result = await client.callTool({ name: 'complex-tool' });
expect(result.structuredContent).toBeDefined();
const structuredContent = result.structuredContent as { name: string; age: number };
expect(structuredContent.name).toBe('John Doe');
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',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
name: 'test-server',
version: '1.0.0',
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'strict-tool',
description: 'A tool with strict schema',
inputSchema: {
type: 'object',
properties: {},
},
outputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
required: ['name'],
additionalProperties: false,
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'strict-tool') {
// Return structured content with extra property
return {
structuredContent: {
name: 'John',
extraField: 'not allowed',
},
};
}
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// List tools to cache the schemas
await client.listTools();
// Call the tool - should throw validation error due to additional property
await expect(client.callTool({ name: 'strict-tool' })).rejects.toThrow(
/Structured content does not match the tool's output schema/
);
});
});

View file

@ -39,7 +39,11 @@ import {
SubscribeRequest,
SUPPORTED_PROTOCOL_VERSIONS,
UnsubscribeRequest,
Tool,
ErrorCode,
McpError,
} from "../types.js";
import { Ajv, type ValidateFunction } from "ajv";
export type ClientOptions = ProtocolOptions & {
/**
@ -86,6 +90,8 @@ export class Client<
private _serverVersion?: Implementation;
private _capabilities: ClientCapabilities;
private _instructions?: string;
private _cachedToolOutputValidators: Map<string, ValidateFunction> = new Map();
private _ajv: InstanceType<typeof Ajv>;
/**
* Initializes this client with the given name and version information.
@ -96,6 +102,7 @@ export class Client<
) {
super(options);
this._capabilities = options?.capabilities ?? {};
this._ajv = new Ajv({ strict: false, validateFormats: true });
}
/**
@ -413,22 +420,84 @@ export class Client<
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
options?: RequestOptions,
) {
return this.request(
const result = await this.request(
{ method: "tools/call", params },
resultSchema,
options,
);
// Check if the tool has an outputSchema
const validator = this.getToolOutputValidator(params.name);
if (validator) {
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
if (!result.structuredContent && !result.isError) {
throw new McpError(
ErrorCode.InvalidRequest,
`Tool ${params.name} has an output schema but did not return structured content`
);
}
// Only validate structured content if present (not when there's an error)
if (result.structuredContent) {
try {
// Validate the structured content (which is already an object) against the schema
const isValid = validator(result.structuredContent);
if (!isValid) {
throw new McpError(
ErrorCode.InvalidParams,
`Structured content does not match the tool's output schema: ${this._ajv.errorsText(validator.errors)}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InvalidParams,
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
);
}
}
}
return result;
}
private cacheToolOutputSchemas(tools: Tool[]) {
this._cachedToolOutputValidators.clear();
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the Ajv validator
if (tool.outputSchema) {
try {
const validator = this._ajv.compile(tool.outputSchema);
this._cachedToolOutputValidators.set(tool.name, validator);
} catch (error) {
console.warn(`Failed to compile output schema for tool ${tool.name}: ${error}`);
}
}
}
}
private getToolOutputValidator(toolName: string): ValidateFunction | undefined {
return this._cachedToolOutputValidators.get(toolName);
}
async listTools(
params?: ListToolsRequest["params"],
options?: RequestOptions,
) {
return this.request(
const result = await this.request(
{ method: "tools/list", params },
ListToolsResultSchema,
options,
);
// Cache the tools and their output schemas for future validation
this.cacheToolOutputSchemas(result.tools);
return result;
}
async sendRootsListChanged() {

View file

@ -61,7 +61,7 @@ async function main(): Promise<void> {
// Log the results from each tool call
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
result.content.forEach((item: { type: string; text?: string; }) => {
result.content?.forEach((item: { type: string; text?: string; }) => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {

View file

@ -341,7 +341,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
});
console.log('Tool result:');
result.content.forEach(item => {
result.content?.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {

View file

@ -173,7 +173,7 @@ async function startNotificationTool(client: Client): Promise<void> {
const result = await client.request(request, CallToolResultSchema);
console.log('Tool result:');
result.content.forEach(item => {
result.content?.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {

View file

@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Client to test outputSchema servers
* This client connects to either the high-level or low-level outputSchema server
* and calls the get_weather tool to demonstrate structured output
*/
import { Client } from "../../client/index.js";
import { StdioClientTransport } from "../../client/stdio.js";
async function main() {
const serverPath = process.argv[2];
if (!serverPath) {
console.error("Usage: npx tsx src/examples/client/testOutputSchemaServers.ts <server-path>");
console.error("Example: npx tsx src/examples/client/testOutputSchemaServers.ts ../server/mcpServerOutputSchema.ts");
process.exit(1);
}
console.log(`Connecting to ${serverPath}...`);
// Create transport that spawns the server process
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", serverPath]
});
const client = new Client({
name: "output-schema-test-client",
version: "1.0.0"
}, {
capabilities: {}
});
try {
await client.connect(transport);
console.log("Connected to server\n");
// List available tools
console.log("Listing available tools...");
const toolsResult = await client.listTools();
console.log("Available tools:");
for (const tool of toolsResult.tools) {
console.log(`- ${tool.name}: ${tool.description}`);
if (tool.outputSchema) {
console.log(" Has outputSchema: Yes");
console.log(" Output schema:", JSON.stringify(tool.outputSchema, null, 2));
} else {
console.log(" Has outputSchema: No");
}
}
// Call the weather tool
console.log("\nCalling get_weather tool...");
// Note: Output schema validation only works when using the high-level Client API
// methods like callTool(). Low-level protocol requests do not validate the response.
const weatherResult = await client.callTool({
name: "get_weather",
arguments: {
city: "London",
country: "UK"
}
});
console.log("\nWeather tool result:");
if (weatherResult.structuredContent) {
console.log("Structured content:");
console.log(JSON.stringify(weatherResult.structuredContent, null, 2));
}
if (weatherResult.content && Array.isArray(weatherResult.content)) {
console.log("Unstructured content:");
weatherResult.content.forEach(content => {
if (content.type === "text") {
console.log(content.text);
}
});
}
await client.close();
console.log("\nDisconnected from server");
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
main();

View file

@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* Example MCP server demonstrating tool outputSchema support using the low-level Server API
* This server manually handles tool listing and invocation requests to return structured data
* For a simpler high-level API approach, see mcpServerOutputSchema.ts
*/
import { Server } from "../../server/index.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { CallToolRequest, CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "../../types.js";
const server = new Server(
{
name: "output-schema-low-level-example",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Tool with structured output
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description: "Get weather information for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
country: { type: "string", description: "Country code (e.g., US, UK)" }
},
required: ["city", "country"]
},
outputSchema: {
type: "object",
properties: {
temperature: {
type: "object",
properties: {
celsius: { type: "number" },
fahrenheit: { type: "number" }
},
required: ["celsius", "fahrenheit"]
},
conditions: {
type: "string",
enum: ["sunny", "cloudy", "rainy", "stormy", "snowy"]
},
humidity: { type: "number", minimum: 0, maximum: 100 },
wind: {
type: "object",
properties: {
speed_kmh: { type: "number" },
direction: { type: "string" }
},
required: ["speed_kmh", "direction"]
}
},
required: ["temperature", "conditions", "humidity", "wind"]
}
}
]
}));
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
switch (request.params.name) {
case "get_weather": {
const { city, country } = request.params.arguments as { city: string; country: string };
// Parameters are available but not used in this example
void city;
void country;
// Simulate weather API call
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
// Return structured content matching the outputSchema
return {
structuredContent: {
temperature: {
celsius: temp_c,
fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10
},
conditions,
humidity: Math.round(Math.random() * 100),
wind: {
speed_kmh: Math.round(Math.random() * 50),
direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)]
}
}
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Low-level Output Schema Example Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});

View file

@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* Example MCP server using the high-level McpServer API with outputSchema
* This demonstrates how to easily create tools with structured output
*/
import { McpServer } from "../../server/mcp.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { z } from "zod";
const server = new McpServer(
{
name: "mcp-output-schema-high-level-example",
version: "1.0.0",
}
);
// Define a tool with structured output - Weather data
server.registerTool(
"get_weather",
{
description: "Get weather information for a city",
inputSchema:{
city: z.string().describe("City name"),
country: z.string().describe("Country code (e.g., US, UK)")
},
outputSchema: {
temperature: z.object({
celsius: z.number(),
fahrenheit: z.number()
}),
conditions: z.enum(["sunny", "cloudy", "rainy", "stormy", "snowy"]),
humidity: z.number().min(0).max(100),
wind: z.object({
speed_kmh: z.number(),
direction: z.string()
})
},
},
async ({ city, country }) => {
// Parameters are available but not used in this example
void city;
void country;
// Simulate weather API call
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
return {
structuredContent: {
temperature: {
celsius: temp_c,
fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10
},
conditions,
humidity: Math.round(Math.random() * 100),
wind: {
speed_kmh: Math.round(Math.random() * 50),
direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)]
}
}
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("High-level Output Schema Example Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});

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.registerTool(
"test (new api)",
{
inputSchema: { name: z.string(), value: z.number() },
},
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,23 @@ describe("tool()", () => {
],
}));
// new api
mcpServer.registerTool(
"test (new api)",
{
description: "Test description",
},
async () => ({
content: [
{
type: "text" as const,
text: "Test response",
},
],
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -490,11 +556,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 +585,21 @@ describe("tool()", () => {
],
}));
mcpServer.registerTool(
"test (new api)",
{
annotations: { title: "Test Tool", readOnlyHint: true },
},
async () => ({
content: [
{
type: "text" as const,
text: "Test response",
},
],
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -529,11 +615,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 +644,17 @@ describe("tool()", () => {
})
);
mcpServer.registerTool(
"test (new api)",
{
inputSchema: { name: z.string() },
annotations: { title: "Test Tool", readOnlyHint: true },
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }]
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -566,15 +668,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 +703,18 @@ describe("tool()", () => {
})
);
mcpServer.registerTool(
"test (new api)",
{
description: "A tool with everything",
inputSchema: { name: z.string() },
annotations: { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false },
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }]
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -608,7 +728,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 +740,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 +769,18 @@ describe("tool()", () => {
})
);
mcpServer.registerTool(
"test (new api)",
{
description: "A tool with everything but empty params",
inputSchema: {},
annotations: { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false },
},
async () => ({
content: [{ type: "text" as const, text: "Test response" }]
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -655,7 +794,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 +806,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 +849,24 @@ describe("tool()", () => {
}),
);
mcpServer.registerTool(
"test (new api)",
{
inputSchema: {
name: z.string(),
value: z.number(),
},
},
async ({ name, value }) => ({
content: [
{
type: "text",
text: `${name}: ${value}`,
},
],
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@ -726,8 +890,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 +938,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 +954,179 @@ 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",
version: "1.0",
});
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {
tools: {},
},
},
);
// Register a tool with outputSchema
mcpServer.registerTool(
"test",
{
description: "Test tool with structured output",
inputSchema: {
input: z.string(),
},
outputSchema: {
processedInput: z.string(),
resultType: z.string(),
timestamp: z.string()
},
},
async ({ input }) => ({
structuredContent: {
processedInput: input,
resultType: "structured",
timestamp: "2023-01-01T00:00:00Z"
},
})
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Verify the tool registration includes outputSchema
const listResult = await client.request(
{
method: "tools/list",
},
ListToolsResultSchema,
);
expect(listResult.tools).toHaveLength(1);
expect(listResult.tools[0].outputSchema).toMatchObject({
type: "object",
properties: {
processedInput: { type: "string" },
resultType: { type: "string" },
timestamp: { type: "string" }
},
required: ["processedInput", "resultType", "timestamp"]
});
// Call the tool and verify it returns valid structuredContent
const result = await client.request(
{
method: "tools/call",
params: {
name: "test",
arguments: {
input: "hello",
},
},
},
CallToolResultSchema,
);
expect(result.structuredContent).toBeDefined();
const structuredContent = result.structuredContent as {
processedInput: string;
resultType: string;
timestamp: string;
};
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.registerTool(
"test",
{
description: "Test tool with invalid structured output",
inputSchema: {
input: z.string(),
},
outputSchema: {
processedInput: z.string(),
resultType: z.string(),
timestamp: z.string()
},
},
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",
@ -821,6 +1180,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",
@ -871,9 +1233,12 @@ describe("tool()", () => {
expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.content[0].text).toContain("Received request ID:");
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(
{
@ -931,6 +1296,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",
@ -994,6 +1362,9 @@ describe("tool()", () => {
]);
});
/***
* Test: Graceful Tool Error Handling
*/
test("should handle server tool errors gracefully", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1043,6 +1414,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",
@ -1093,6 +1467,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",
@ -1132,6 +1509,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",
@ -1194,6 +1574,9 @@ describe("resource()", () => {
expect(notifications).toHaveLength(0);
});
/***
* Test: Update Resource Template
*/
test("should update resource template", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1260,6 +1643,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",
@ -1313,6 +1699,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",
@ -1374,6 +1763,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",
@ -1438,6 +1830,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",
@ -1485,6 +1880,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",
@ -1530,6 +1928,9 @@ describe("resource()", () => {
);
});
/***
* Test: Resource Template with List Callback
*/
test("should register resource template with listCallback", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -1588,6 +1989,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",
@ -1634,6 +2038,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",
@ -1661,6 +2068,9 @@ describe("resource()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Multiple Resource Registration
*/
test("should allow registering multiple resources", () => {
const mcpServer = new McpServer({
name: "test server",
@ -1688,6 +2098,9 @@ describe("resource()", () => {
}));
});
/***
* Test: Preventing Duplicate Resource Template Registration
*/
test("should prevent duplicate resource template registration", () => {
const mcpServer = new McpServer({
name: "test server",
@ -1723,6 +2136,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",
@ -1758,6 +2174,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",
@ -1798,6 +2217,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",
@ -1863,6 +2285,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",
@ -1931,6 +2356,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",
@ -1986,6 +2414,9 @@ describe("resource()", () => {
});
describe("prompt()", () => {
/***
* Test: Zero-Argument Prompt Registration
*/
test("should register zero-argument prompt", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2027,6 +2458,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",
@ -2095,6 +2529,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",
@ -2188,6 +2625,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",
@ -2247,6 +2687,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",
@ -2325,6 +2768,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",
@ -2377,6 +2823,9 @@ describe("prompt()", () => {
]);
});
/***
* Test: Prompt Registration with Description
*/
test("should register prompt with description", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -2419,6 +2868,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",
@ -2481,6 +2933,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",
@ -2514,6 +2969,9 @@ describe("prompt()", () => {
}).toThrow(/already registered/);
});
/***
* Test: Multiple Prompt Registration
*/
test("should allow registering multiple prompts", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2547,6 +3005,9 @@ describe("prompt()", () => {
}));
});
/***
* Test: Prompt Registration with Arguments
*/
test("should allow registering prompts with arguments", () => {
const mcpServer = new McpServer({
name: "test server",
@ -2569,6 +3030,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",
@ -2610,6 +3074,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",
@ -2661,6 +3128,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",
@ -2726,6 +3196,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",
@ -2793,6 +3266,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,
);
@ -111,16 +111,25 @@ export class McpServer {
([, tool]) => tool.enabled,
).map(
([name, tool]): Tool => {
return {
const toolDefinition: Tool = {
name,
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,
};
if (tool.outputSchema) {
toolDefinition.outputSchema = zodToJsonSchema(
tool.outputSchema,
{ strictUnions: true }
) as Tool["outputSchema"];
}
return toolDefinition;
},
),
}),
@ -144,6 +153,8 @@ export class McpServer {
);
}
let result: CallToolResult;
if (tool.inputSchema) {
const parseResult = await tool.inputSchema.safeParseAsync(
request.params.arguments,
@ -158,9 +169,9 @@ export class McpServer {
const args = parseResult.data;
const cb = tool.callback as ToolCallback<ZodRawShape>;
try {
return await Promise.resolve(cb(args, extra));
result = await Promise.resolve(cb(args, extra));
} catch (error) {
return {
result = {
content: [
{
type: "text",
@ -173,9 +184,9 @@ export class McpServer {
} else {
const cb = tool.callback as ToolCallback<undefined>;
try {
return await Promise.resolve(cb(extra));
result = await Promise.resolve(cb(extra));
} catch (error) {
return {
result = {
content: [
{
type: "text",
@ -186,6 +197,46 @@ export class McpServer {
};
}
}
// Handle structured output and backward compatibility
if (tool.outputSchema) {
// Tool has outputSchema, so result must have structuredContent (unless it's an error)
if (!result.structuredContent && !result.isError) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has outputSchema but returned no structuredContent`,
);
}
// For backward compatibility, if structuredContent is provided but no content,
// automatically serialize the structured content to text
if (result.structuredContent && !result.content) {
result.content = [
{
type: "text",
text: JSON.stringify(result.structuredContent, null, 2),
},
];
}
} else {
// Tool must have content if no outputSchema
if (!result.content && !result.isError) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema and must return content`,
);
}
// If structuredContent is provided, it's an error
if (result.structuredContent) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema but returned structuredContent`,
);
}
}
return result;
},
);
@ -398,7 +449,7 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._resourceHandlersInitialized = true;
}
@ -481,7 +532,7 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._promptHandlersInitialized = true;
}
@ -596,6 +647,47 @@ export class McpServer {
}
}
private _createRegisteredTool(
name: string,
description: string | undefined,
inputSchema: ZodRawShape | undefined,
outputSchema: ZodRawShape | undefined,
annotations: ToolAnnotations | undefined,
callback: ToolCallback<ZodRawShape | undefined>
): RegisteredTool {
const registeredTool: RegisteredTool = {
description,
inputSchema:
inputSchema === undefined ? undefined : z.object(inputSchema),
outputSchema:
outputSchema === undefined ? undefined : z.object(outputSchema),
annotations,
callback,
enabled: true,
disable: () => registeredTool.update({ enabled: false }),
enable: () => registeredTool.update({ enabled: true }),
remove: () => registeredTool.update({ name: null }),
update: (updates) => {
if (typeof updates.name !== "undefined" && updates.name !== name) {
delete this._registeredTools[name]
if (updates.name) this._registeredTools[updates.name] = registeredTool
}
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
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
this.sendToolListChanged()
},
};
this._registeredTools[name] = registeredTool;
this.setToolRequestHandlers();
this.sendToolListChanged()
return registeredTool
}
/**
* Registers a zero-argument tool `name`, which will run the given function when the client calls it.
*/
@ -633,7 +725,7 @@ export class McpServer {
paramsSchemaOrAnnotations: Args | ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with both parameter schema and annotations.
*/
@ -643,7 +735,7 @@ export class McpServer {
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with description, parameter schema, and annotations.
*/
@ -655,29 +747,38 @@ export class McpServer {
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* tool() implementation. Parses arguments passed to overrides defined above.
*/
tool(name: string, ...rest: unknown[]): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
}
let description: string | undefined;
let inputSchema: ZodRawShape | undefined;
let outputSchema: ZodRawShape | undefined;
let annotations: ToolAnnotations | undefined;
// 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;
}
let paramsSchema: 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
// 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
paramsSchema = rest.shift() as ZodRawShape;
// Check if the next arg is potentially annotations
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)
@ -690,37 +791,38 @@ export class McpServer {
annotations = rest.shift() as ToolAnnotations;
}
}
const callback = rest[0] as ToolCallback<ZodRawShape | undefined>;
const cb = rest[0] as ToolCallback<ZodRawShape | undefined>;
const registeredTool: RegisteredTool = {
return this._createRegisteredTool(name, description, inputSchema, outputSchema, annotations, callback)
}
/**
* Registers a tool with a config object and callback.
*/
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
name: string,
config: {
description?: string;
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
},
cb: ToolCallback<InputArgs>
): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
}
const { description, inputSchema, outputSchema, annotations } = config;
return this._createRegisteredTool(
name,
description,
inputSchema:
paramsSchema === undefined ? undefined : z.object(paramsSchema),
inputSchema,
outputSchema,
annotations,
callback: cb,
enabled: true,
disable: () => registeredTool.update({ enabled: false }),
enable: () => registeredTool.update({ enabled: true }),
remove: () => registeredTool.update({ name: null }),
update: (updates) => {
if (typeof updates.name !== "undefined" && updates.name !== name) {
delete this._registeredTools[name]
if (updates.name) this._registeredTools[updates.name] = registeredTool
}
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
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
this.sendToolListChanged()
},
};
this._registeredTools[name] = registeredTool;
this.setToolRequestHandlers();
this.sendToolListChanged()
return registeredTool
cb as ToolCallback<ZodRawShape | undefined>
)
}
/**
@ -896,24 +998,39 @@ export class ResourceTemplate {
* Callback for a tool handler registered with Server.tool().
*
* Parameters will include tool arguments, if applicable, as well as other request handler context.
*
* The callback should return:
* - `structuredContent` if the tool has an outputSchema defined
* - `content` if the tool does not have an outputSchema
* - Both fields are optional but typically one should be provided
*/
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?: 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, 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
};
@ -986,23 +1103,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

@ -829,8 +829,23 @@ export const ToolSchema = z
.object({
type: z.literal("object"),
properties: z.optional(z.object({}).passthrough()),
required: z.optional(z.array(z.string())),
})
.passthrough(),
/**
* An optional JSON Schema object defining the structure of the tool's output.
*
* 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({
type: z.literal("object"),
properties: z.optional(z.object({}).passthrough()),
required: z.optional(z.array(z.string())),
})
.passthrough()
),
/**
* Optional additional tool information.
*/
@ -854,14 +869,79 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
/**
* The server's response to a tool call.
*
* Any errors that originate from the tool SHOULD be reported inside the result
* object, with `isError` set to true, _not_ as an MCP protocol-level error
* response. Otherwise, the LLM would not be able to see that an error occurred
* and self-correct.
*
* However, any errors in _finding_ the tool, an error indicating that the
* server does not support tool calls, or any other exceptional conditions,
* should be reported as an MCP error response.
*/
export const CallToolResultSchema = ResultSchema.extend({
content: z.array(
z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]),
),
isError: z.boolean().default(false).optional(),
export const ContentListSchema = z.array(
z.union([
TextContentSchema,
ImageContentSchema,
AudioContentSchema,
EmbeddedResourceSchema,
]),
);
export const CallToolUnstructuredResultSchema = ResultSchema.extend({
/**
* A list of content objects that represent the result of the tool call.
*
* If the Tool does not define an outputSchema, this field MUST be present in the result.
*/
content: ContentListSchema,
/**
* Structured output must not be provided in an unstructured tool result.
*/
structuredContent: z.never().optional(),
/**
* Whether the tool call ended in an error.
*
* If not set, this is assumed to be false (the call was successful).
*/
isError: z.optional(z.boolean()),
});
export const CallToolStructuredResultSchema = ResultSchema.extend({
/**
* An object containing structured tool output.
*
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
*/
structuredContent: z.object({}).passthrough(),
/**
* A list of content objects that represent the result of the tool call.
*
* If the Tool defines an outputSchema, this field MAY be present in the result.
*
* Tools may use this field to provide compatibility with older clients that
* do not support structured content.
*
* Clients that support structured content should ignore this field.
*/
content: z.optional(ContentListSchema),
/**
* Whether the tool call ended in an error.
*
* If not set, this is assumed to be false (the call was successful).
*/
isError: z.optional(z.boolean()),
});
export const CallToolResultSchema = z.union([
CallToolUnstructuredResultSchema,
CallToolStructuredResultSchema,
]);
/**
* CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07.
*/
@ -1312,6 +1392,9 @@ export type ToolAnnotations = Infer<typeof ToolAnnotationsSchema>;
export type Tool = Infer<typeof ToolSchema>;
export type ListToolsRequest = Infer<typeof ListToolsRequestSchema>;
export type ListToolsResult = Infer<typeof ListToolsResultSchema>;
export type ContentList = Infer<typeof ContentListSchema>;
export type CallToolUnstructuredResult = Infer<typeof CallToolUnstructuredResultSchema>;
export type CallToolStructuredResult = Infer<typeof CallToolStructuredResultSchema>;
export type CallToolResult = Infer<typeof CallToolResultSchema>;
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;