Merge branch 'main' into fix/1
This commit is contained in:
commit
c5dd44c4d6
8 changed files with 289 additions and 19 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/sdk",
|
||||
"version": "1.10.2",
|
||||
"version": "1.11.1",
|
||||
"description": "Model Context Protocol implementation for TypeScript",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe("OAuth Authorization", () => {
|
|||
const [url, options] = calls[0];
|
||||
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
|
||||
expect(options.headers).toEqual({
|
||||
"MCP-Protocol-Version": "2024-11-05"
|
||||
"MCP-Protocol-Version": "2025-03-26"
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -478,4 +478,4 @@ describe("OAuth Authorization", () => {
|
|||
).rejects.toThrow("Dynamic client registration failed");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -165,6 +165,194 @@ test("should reject unsupported protocol version", async () => {
|
|||
expect(clientTransport.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should connect new client to old, supported server version", async () => {
|
||||
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
|
||||
protocolVersion: OLD_VERSION,
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: "old server",
|
||||
version: "1.0",
|
||||
},
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, () => ({
|
||||
resources: [],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
||||
tools: [],
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "new client",
|
||||
version: "1.0",
|
||||
protocolVersion: LATEST_PROTOCOL_VERSION,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
expect(client.getServerVersion()).toEqual({
|
||||
name: "old server",
|
||||
version: "1.0",
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
{
|
||||
name: "new server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
|
||||
protocolVersion: LATEST_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: "new server",
|
||||
version: "1.0",
|
||||
},
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, () => ({
|
||||
resources: [],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
||||
tools: [],
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "old client",
|
||||
version: "1.0",
|
||||
protocolVersion: OLD_VERSION,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
expect(client.getServerVersion()).toEqual({
|
||||
name: "new server",
|
||||
version: "1.0",
|
||||
});
|
||||
});
|
||||
|
||||
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";
|
||||
const server = new Server(
|
||||
{
|
||||
name: "new server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
|
||||
protocolVersion: FUTURE_VERSION,
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: "new server",
|
||||
version: "1.0",
|
||||
},
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, () => ({
|
||||
resources: [],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
||||
tools: [],
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "old client",
|
||||
version: "1.0",
|
||||
protocolVersion: OLD_VERSION,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
expect(client.connect(clientTransport)).rejects.toThrow(
|
||||
"Server's protocol version is not supported: FUTURE_VERSION"
|
||||
),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test("should respect server capabilities", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ChildProcess, IOType } from "node:child_process";
|
||||
import spawn from "cross-spawn";
|
||||
import process from "node:process";
|
||||
import { Stream } from "node:stream";
|
||||
import { Stream, PassThrough } from "node:stream";
|
||||
import { ReadBuffer, serializeMessage } from "../shared/stdio.js";
|
||||
import { Transport } from "../shared/transport.js";
|
||||
import { JSONRPCMessage } from "../types.js";
|
||||
|
|
@ -93,6 +93,7 @@ export class StdioClientTransport implements Transport {
|
|||
private _abortController: AbortController = new AbortController();
|
||||
private _readBuffer: ReadBuffer = new ReadBuffer();
|
||||
private _serverParams: StdioServerParameters;
|
||||
private _stderrStream: PassThrough | null = null;
|
||||
|
||||
onclose?: () => void;
|
||||
onerror?: (error: Error) => void;
|
||||
|
|
@ -100,6 +101,9 @@ export class StdioClientTransport implements Transport {
|
|||
|
||||
constructor(server: StdioServerParameters) {
|
||||
this._serverParams = server;
|
||||
if (server.stderr === "pipe" || server.stderr === "overlapped") {
|
||||
this._stderrStream = new PassThrough();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,15 +166,25 @@ export class StdioClientTransport implements Transport {
|
|||
this._process.stdout?.on("error", (error) => {
|
||||
this.onerror?.(error);
|
||||
});
|
||||
|
||||
if (this._stderrStream && this._process.stderr) {
|
||||
this._process.stderr.pipe(this._stderrStream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
|
||||
*
|
||||
* This is only available after the process has been started.
|
||||
* If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to
|
||||
* attach listeners before the start method is invoked. This prevents loss of any early
|
||||
* error output emitted by the child process.
|
||||
*/
|
||||
get stderr(): Stream | null {
|
||||
if (this._stderrStream) {
|
||||
return this._stderrStream;
|
||||
}
|
||||
|
||||
return this._process?.stderr ?? null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -622,6 +622,53 @@ describe("tool()", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("should register tool with description, empty params, and annotations", async () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
mcpServer.tool(
|
||||
"test",
|
||||
"A tool with everything but empty params",
|
||||
{},
|
||||
{ title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false },
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Test response" }]
|
||||
})
|
||||
);
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const result = await client.request(
|
||||
{ method: "tools/list" },
|
||||
ListToolsResultSchema,
|
||||
);
|
||||
|
||||
expect(result.tools).toHaveLength(1);
|
||||
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({
|
||||
type: "object",
|
||||
properties: {}
|
||||
});
|
||||
expect(result.tools[0].annotations).toEqual({
|
||||
title: "Complete Test Tool with empty params",
|
||||
readOnlyHint: true,
|
||||
openWorldHint: false
|
||||
});
|
||||
});
|
||||
|
||||
test("should validate tool args", async () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "test server",
|
||||
|
|
|
|||
|
|
@ -663,8 +663,11 @@ export class McpServer {
|
|||
// Helper to check if an object is a Zod schema (ZodRawShape)
|
||||
const isZodRawShape = (obj: unknown): obj is ZodRawShape => {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
// Check that at least one property is a ZodType instance
|
||||
return Object.values(obj as object).some(v => v instanceof ZodType);
|
||||
|
||||
const isEmptyObject = z.object({}).strict().safeParse(obj).success;
|
||||
|
||||
// Check if object is empty or at least one property is a ZodType instance
|
||||
return isEmptyObject || Object.values(obj as object).some(v => v instanceof ZodType);
|
||||
};
|
||||
|
||||
let description: string | undefined;
|
||||
|
|
|
|||
17
src/types.test.ts
Normal file
17
src/types.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "./types.js";
|
||||
|
||||
describe("Types", () => {
|
||||
|
||||
test("should have correct latest protocol version", () => {
|
||||
expect(LATEST_PROTOCOL_VERSION).toBeDefined();
|
||||
expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26");
|
||||
});
|
||||
test("should have correct supported protocol versions", () => {
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined();
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeInstanceOf(Array);
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION);
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-11-05");
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-10-07");
|
||||
});
|
||||
|
||||
});
|
||||
25
src/types.ts
25
src/types.ts
|
|
@ -1,8 +1,9 @@
|
|||
import { z, ZodTypeAny } from "zod";
|
||||
|
||||
export const LATEST_PROTOCOL_VERSION = "2024-11-05";
|
||||
export const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
||||
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
"2024-11-05",
|
||||
"2024-10-07",
|
||||
];
|
||||
|
||||
|
|
@ -754,11 +755,11 @@ export const PromptListChangedNotificationSchema = NotificationSchema.extend({
|
|||
/* Tools */
|
||||
/**
|
||||
* Additional properties describing a Tool to clients.
|
||||
*
|
||||
* NOTE: all properties in ToolAnnotations are **hints**.
|
||||
* They are not guaranteed to provide a faithful description of
|
||||
*
|
||||
* NOTE: all properties in ToolAnnotations are **hints**.
|
||||
* They are not guaranteed to provide a faithful description of
|
||||
* tool behavior (including descriptive properties like `title`).
|
||||
*
|
||||
*
|
||||
* Clients should never make tool use decisions based on ToolAnnotations
|
||||
* received from untrusted servers.
|
||||
*/
|
||||
|
|
@ -771,7 +772,7 @@ export const ToolAnnotationsSchema = z
|
|||
|
||||
/**
|
||||
* If true, the tool does not modify its environment.
|
||||
*
|
||||
*
|
||||
* Default: false
|
||||
*/
|
||||
readOnlyHint: z.optional(z.boolean()),
|
||||
|
|
@ -779,19 +780,19 @@ export const ToolAnnotationsSchema = z
|
|||
/**
|
||||
* If true, the tool may perform destructive updates to its environment.
|
||||
* If false, the tool performs only additive updates.
|
||||
*
|
||||
*
|
||||
* (This property is meaningful only when `readOnlyHint == false`)
|
||||
*
|
||||
*
|
||||
* Default: true
|
||||
*/
|
||||
destructiveHint: z.optional(z.boolean()),
|
||||
|
||||
/**
|
||||
* If true, calling the tool repeatedly with the same arguments
|
||||
* If true, calling the tool repeatedly with the same arguments
|
||||
* will have no additional effect on the its environment.
|
||||
*
|
||||
*
|
||||
* (This property is meaningful only when `readOnlyHint == false`)
|
||||
*
|
||||
*
|
||||
* Default: false
|
||||
*/
|
||||
idempotentHint: z.optional(z.boolean()),
|
||||
|
|
@ -801,7 +802,7 @@ export const ToolAnnotationsSchema = z
|
|||
* entities. If false, the tool's domain of interaction is closed.
|
||||
* For example, the world of a web search tool is open, whereas that
|
||||
* of a memory tool is not.
|
||||
*
|
||||
*
|
||||
* Default: true
|
||||
*/
|
||||
openWorldHint: z.optional(z.boolean()),
|
||||
|
|
|
|||
Loading…
Reference in a new issue