Merge pull request #454 from modelcontextprotocol/basil/output_schema
Prototype support for `outputSchema`/`structuredContent`
This commit is contained in:
commit
1fd6265751
13 changed files with 1706 additions and 97 deletions
103
package-lock.json
generated
103
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
89
src/examples/client/testOutputSchemaServers.ts
Executable file
89
src/examples/client/testOutputSchemaServers.ts
Executable 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();
|
||||
116
src/examples/server/lowLevelOutputSchema.ts
Normal file
116
src/examples/server/lowLevelOutputSchema.ts
Normal 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);
|
||||
});
|
||||
74
src/examples/server/mcpServerOutputSchema.ts
Normal file
74
src/examples/server/mcpServerOutputSchema.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
93
src/types.ts
93
src/types.ts
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue