Skip to content

Commit f31cefe

Browse files
authored
Merge branch 'main' into fix/1
2 parents afb128d + 1b14bd7 commit f31cefe

File tree

11 files changed

+246
-57
lines changed

11 files changed

+246
-57
lines changed

README.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Tools](#tools)
1313
- [Prompts](#prompts)
1414
- [Completions](#completions)
15+
- [Sampling](#sampling)
1516
- [Running Your Server](#running-your-server)
1617
- [stdio](#stdio)
1718
- [Streamable HTTP](#streamable-http)
@@ -44,6 +45,8 @@ The Model Context Protocol allows applications to provide context for LLMs in a
4445
npm install @modelcontextprotocol/sdk
4546
```
4647

48+
> ⚠️ MCP requires Node v18.x up to work fine.
49+
4750
## Quick Start
4851

4952
Let's create a simple MCP server that exposes a calculator tool and some data:
@@ -382,6 +385,68 @@ import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.j
382385
const displayName = getDisplayName(tool);
383386
```
384387

388+
### Sampling
389+
390+
MCP servers can request LLM completions from connected clients that support sampling.
391+
392+
```typescript
393+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
394+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
395+
import { z } from "zod";
396+
397+
const mcpServer = new McpServer({
398+
name: "tools-with-sample-server",
399+
version: "1.0.0",
400+
});
401+
402+
// Tool that uses LLM sampling to summarize any text
403+
mcpServer.registerTool(
404+
"summarize",
405+
{
406+
description: "Summarize any text using an LLM",
407+
inputSchema: {
408+
text: z.string().describe("Text to summarize"),
409+
},
410+
},
411+
async ({ text }) => {
412+
// Call the LLM through MCP sampling
413+
const response = await mcpServer.server.createMessage({
414+
messages: [
415+
{
416+
role: "user",
417+
content: {
418+
type: "text",
419+
text: `Please summarize the following text concisely:\n\n${text}`,
420+
},
421+
},
422+
],
423+
maxTokens: 500,
424+
});
425+
426+
return {
427+
content: [
428+
{
429+
type: "text",
430+
text: response.content.type === "text" ? response.content.text : "Unable to generate summary",
431+
},
432+
],
433+
};
434+
}
435+
);
436+
437+
async function main() {
438+
const transport = new StdioServerTransport();
439+
await mcpServer.connect(transport);
440+
console.log("MCP server is running...");
441+
}
442+
443+
main().catch((error) => {
444+
console.error("Server error:", error);
445+
process.exit(1);
446+
});
447+
```
448+
449+
385450
## Running Your Server
386451

387452
MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport:
@@ -588,8 +653,17 @@ app.delete('/mcp', async (req: Request, res: Response) => {
588653
589654
// Start the server
590655
const PORT = 3000;
591-
app.listen(PORT, () => {
592-
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
656+
setupServer().then(() => {
657+
app.listen(PORT, (error) => {
658+
if (error) {
659+
console.error('Failed to start server:', error);
660+
process.exit(1);
661+
}
662+
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
663+
});
664+
}).catch(error => {
665+
console.error('Failed to set up the server:', error);
666+
process.exit(1);
593667
});
594668
595669
```

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.13.2",
3+
"version": "1.13.3",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

src/client/auth.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,19 @@ describe("OAuth Authorization", () => {
403403
expect(mockFetch).toHaveBeenCalledTimes(2);
404404
});
405405

406+
it("returns undefined when both CORS requests fail in fetchWithCorsRetry", async () => {
407+
// fetchWithCorsRetry tries with headers (fails with CORS), then retries without headers (also fails with CORS)
408+
// simulating a 404 w/o headers set. We want this to return undefined, not throw TypeError
409+
mockFetch.mockImplementation(() => {
410+
// Both the initial request with headers and retry without headers fail with CORS TypeError
411+
return Promise.reject(new TypeError("Failed to fetch"));
412+
});
413+
414+
// This should return undefined (the desired behavior after the fix)
415+
const metadata = await discoverOAuthMetadata("https://auth.example.com/path");
416+
expect(metadata).toBeUndefined();
417+
});
418+
406419
it("returns undefined when discovery endpoint returns 404", async () => {
407420
mockFetch.mockResolvedValueOnce({
408421
ok: false,

src/client/auth.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -292,25 +292,24 @@ export async function discoverOAuthProtectedResourceMetadata(
292292
return OAuthProtectedResourceMetadataSchema.parse(await response.json());
293293
}
294294

295-
/**
296-
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
297-
*
298-
* If the server returns a 404 for the well-known endpoint, this function will
299-
* return `undefined`. Any other errors will be thrown as exceptions.
300-
*/
301295
/**
302296
* Helper function to handle fetch with CORS retry logic
303297
*/
304298
async function fetchWithCorsRetry(
305299
url: URL,
306-
headers: Record<string, string>,
307-
): Promise<Response> {
300+
headers?: Record<string, string>,
301+
): Promise<Response | undefined> {
308302
try {
309303
return await fetch(url, { headers });
310304
} catch (error) {
311-
// CORS errors come back as TypeError, retry without headers
312305
if (error instanceof TypeError) {
313-
return await fetch(url);
306+
if (headers) {
307+
// CORS errors come back as TypeError, retry without headers
308+
return fetchWithCorsRetry(url)
309+
} else {
310+
// We're getting CORS errors on retry too, return undefined
311+
return undefined
312+
}
314313
}
315314
throw error;
316315
}
@@ -334,7 +333,7 @@ function buildWellKnownPath(pathname: string): string {
334333
async function tryMetadataDiscovery(
335334
url: URL,
336335
protocolVersion: string,
337-
): Promise<Response> {
336+
): Promise<Response | undefined> {
338337
const headers = {
339338
"MCP-Protocol-Version": protocolVersion
340339
};
@@ -344,10 +343,16 @@ async function tryMetadataDiscovery(
344343
/**
345344
* Determines if fallback to root discovery should be attempted
346345
*/
347-
function shouldAttemptFallback(response: Response, pathname: string): boolean {
348-
return response.status === 404 && pathname !== '/';
346+
function shouldAttemptFallback(response: Response | undefined, pathname: string): boolean {
347+
return !response || response.status === 404 && pathname !== '/';
349348
}
350349

350+
/**
351+
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
352+
*
353+
* If the server returns a 404 for the well-known endpoint, this function will
354+
* return `undefined`. Any other errors will be thrown as exceptions.
355+
*/
351356
export async function discoverOAuthMetadata(
352357
authorizationServerUrl: string | URL,
353358
opts?: { protocolVersion?: string },
@@ -362,18 +367,10 @@ export async function discoverOAuthMetadata(
362367

363368
// If path-aware discovery fails with 404, try fallback to root discovery
364369
if (shouldAttemptFallback(response, issuer.pathname)) {
365-
try {
366-
const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
367-
response = await tryMetadataDiscovery(rootUrl, protocolVersion);
368-
369-
if (response.status === 404) {
370-
return undefined;
371-
}
372-
} catch {
373-
// If fallback fails, return undefined
374-
return undefined;
375-
}
376-
} else if (response.status === 404) {
370+
const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
371+
response = await tryMetadataDiscovery(rootUrl, protocolVersion);
372+
}
373+
if (!response || response.status === 404) {
377374
return undefined;
378375
}
379376

src/client/stdio.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ test("should read messages", async () => {
7474
await client.close();
7575
});
7676

77+
7778
test("should properly set default environment variables in spawned process", async () => {
7879
await envAsyncLocalStorage.run({ env: {} }, async () => {
7980
const client = new StdioClientTransport(serverParameters);
@@ -128,4 +129,13 @@ test("should override default environment variables with custom ones", async ()
128129
}
129130
}
130131
});
132+
133+
test("should return child process pid", async () => {
134+
const client = new StdioClientTransport(serverParameters);
135+
136+
await client.start();
137+
expect(client.pid).not.toBeNull();
138+
await client.close();
139+
expect(client.pid).toBeNull();
140+
131141
});

src/client/stdio.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ export class StdioClientTransport implements Transport {
189189
return this._process?.stderr ?? null;
190190
}
191191

192+
/**
193+
* The child process pid spawned by this transport.
194+
*
195+
* This is only available after the transport has been started.
196+
*/
197+
get pid(): number | null {
198+
return this._process?.pid ?? null;
199+
}
200+
192201
private processReadBuffer() {
193202
while (true) {
194203
try {

src/examples/server/mcpServerOutputSchema.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ server.registerTool(
4343
void country;
4444
// Simulate weather API call
4545
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
46-
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
46+
const conditionCandidates = [
47+
"sunny",
48+
"cloudy",
49+
"rainy",
50+
"stormy",
51+
"snowy",
52+
] as const;
53+
const conditions = conditionCandidates[Math.floor(Math.random() * conditionCandidates.length)];
4754

4855
const structuredContent = {
4956
temperature: {
@@ -77,4 +84,4 @@ async function main() {
7784
main().catch((error) => {
7885
console.error("Server error:", error);
7986
process.exit(1);
80-
});
87+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
// Run with: npx tsx src/examples/server/toolWithSampleServer.ts
3+
4+
import { McpServer } from "../../server/mcp.js";
5+
import { StdioServerTransport } from "../../server/stdio.js";
6+
import { z } from "zod";
7+
8+
const mcpServer = new McpServer({
9+
name: "tools-with-sample-server",
10+
version: "1.0.0",
11+
});
12+
13+
// Tool that uses LLM sampling to summarize any text
14+
mcpServer.registerTool(
15+
"summarize",
16+
{
17+
description: "Summarize any text using an LLM",
18+
inputSchema: {
19+
text: z.string().describe("Text to summarize"),
20+
},
21+
},
22+
async ({ text }) => {
23+
// Call the LLM through MCP sampling
24+
const response = await mcpServer.server.createMessage({
25+
messages: [
26+
{
27+
role: "user",
28+
content: {
29+
type: "text",
30+
text: `Please summarize the following text concisely:\n\n${text}`,
31+
},
32+
},
33+
],
34+
maxTokens: 500,
35+
});
36+
37+
return {
38+
content: [
39+
{
40+
type: "text",
41+
text: response.content.type === "text" ? response.content.text : "Unable to generate summary",
42+
},
43+
],
44+
};
45+
}
46+
);
47+
48+
async function main() {
49+
const transport = new StdioServerTransport();
50+
await mcpServer.connect(transport);
51+
console.log("MCP server is running...");
52+
}
53+
54+
main().catch((error) => {
55+
console.error("Server error:", error);
56+
process.exit(1);
57+
});

src/server/mcp.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ describe("tool()", () => {
267267
expect(result.tools[0].name).toBe("test");
268268
expect(result.tools[0].inputSchema).toEqual({
269269
type: "object",
270+
properties: {},
270271
});
271272

272273
// Adding the tool before the connection was established means no notification was sent
@@ -1311,7 +1312,7 @@ describe("tool()", () => {
13111312
resultType: "structured",
13121313
// Missing required 'timestamp' field
13131314
someExtraField: "unexpected" // Extra field not in schema
1314-
},
1315+
} as unknown as { processedInput: string; resultType: string; timestamp: string }, // Type assertion to bypass TypeScript validation for testing purposes
13151316
})
13161317
);
13171318

0 commit comments

Comments
 (0)