Merge branch 'main' into fix/1

This commit is contained in:
오병진 2025-05-09 14:27:41 +09:00 • committed by GitHub
commit c5dd44c4d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 289 additions and 19 deletions

View file

@ -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)",

View file

@ -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");
});
});
});
});

View file

@ -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(
{

View file

@ -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;
}

View file

@ -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",

View file

@ -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
View 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");
});
});

View file

@ -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()),