@modelcontextprotocol/sdk 1.19.1 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +423 -263
- package/dist/cjs/client/auth.d.ts.map +1 -1
- package/dist/cjs/client/auth.js +14 -16
- package/dist/cjs/client/auth.js.map +1 -1
- package/dist/cjs/shared/auth.d.ts +8 -4
- package/dist/cjs/shared/auth.d.ts.map +1 -1
- package/dist/cjs/shared/auth.js +7 -3
- package/dist/cjs/shared/auth.js.map +1 -1
- package/dist/esm/client/auth.d.ts.map +1 -1
- package/dist/esm/client/auth.js +14 -16
- package/dist/esm/client/auth.js.map +1 -1
- package/dist/esm/shared/auth.d.ts +8 -4
- package/dist/esm/shared/auth.d.ts.map +1 -1
- package/dist/esm/shared/auth.js +6 -2
- package/dist/esm/shared/auth.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
# MCP TypeScript SDK  
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<details>
|
|
4
|
+
<summary>Table of Contents</summary>
|
|
4
5
|
|
|
5
6
|
- [Overview](#overview)
|
|
6
7
|
- [Installation](#installation)
|
|
7
|
-
- [
|
|
8
|
-
- [What is MCP?](#what-is-mcp)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
9
|
- [Core Concepts](#core-concepts)
|
|
10
10
|
- [Server](#server)
|
|
11
|
-
- [Resources](#resources)
|
|
12
11
|
- [Tools](#tools)
|
|
12
|
+
- [Resources](#resources)
|
|
13
13
|
- [Prompts](#prompts)
|
|
14
14
|
- [Completions](#completions)
|
|
15
|
+
- [Display Names and Metadata](#display-names-and-metadata)
|
|
15
16
|
- [Sampling](#sampling)
|
|
16
17
|
- [Running Your Server](#running-your-server)
|
|
17
|
-
- [stdio](#stdio)
|
|
18
18
|
- [Streamable HTTP](#streamable-http)
|
|
19
|
+
- [stdio](#stdio)
|
|
19
20
|
- [Testing and Debugging](#testing-and-debugging)
|
|
20
21
|
- [Examples](#examples)
|
|
21
22
|
- [Echo Server](#echo-server)
|
|
22
23
|
- [SQLite Explorer](#sqlite-explorer)
|
|
23
24
|
- [Advanced Usage](#advanced-usage)
|
|
24
25
|
- [Dynamic Servers](#dynamic-servers)
|
|
26
|
+
- [Improving Network Efficiency with Notification Debouncing](#improving-network-efficiency-with-notification-debouncing)
|
|
25
27
|
- [Low-Level Server](#low-level-server)
|
|
28
|
+
- [Eliciting User Input](#eliciting-user-input)
|
|
26
29
|
- [Writing MCP Clients](#writing-mcp-clients)
|
|
27
30
|
- [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream)
|
|
28
31
|
- [Backwards Compatibility](#backwards-compatibility)
|
|
@@ -30,14 +33,16 @@
|
|
|
30
33
|
- [Contributing](#contributing)
|
|
31
34
|
- [License](#license)
|
|
32
35
|
|
|
36
|
+
</details>
|
|
37
|
+
|
|
33
38
|
## Overview
|
|
34
39
|
|
|
35
|
-
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements
|
|
40
|
+
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements
|
|
41
|
+
[the full MCP specification](https://modelcontextprotocol.io/specification/latest), making it easy to:
|
|
36
42
|
|
|
37
|
-
- Build MCP clients that can connect to any MCP server
|
|
38
43
|
- Create MCP servers that expose resources, prompts and tools
|
|
44
|
+
- Build MCP clients that can connect to any MCP server
|
|
39
45
|
- Use standard transports like stdio and Streamable HTTP
|
|
40
|
-
- Handle all MCP protocol messages and lifecycle events
|
|
41
46
|
|
|
42
47
|
## Installation
|
|
43
48
|
|
|
@@ -45,15 +50,14 @@ The Model Context Protocol allows applications to provide context for LLMs in a
|
|
|
45
50
|
npm install @modelcontextprotocol/sdk
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
> ⚠️ MCP requires Node.js v18.x or higher to work fine.
|
|
49
|
-
|
|
50
53
|
## Quick Start
|
|
51
54
|
|
|
52
|
-
Let's create a simple MCP server that exposes a calculator tool and some data
|
|
55
|
+
Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as `server.ts`:
|
|
53
56
|
|
|
54
57
|
```typescript
|
|
55
58
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
56
|
-
import {
|
|
59
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
60
|
+
import express from 'express';
|
|
57
61
|
import { z } from 'zod';
|
|
58
62
|
|
|
59
63
|
// Create an MCP server
|
|
@@ -68,11 +72,16 @@ server.registerTool(
|
|
|
68
72
|
{
|
|
69
73
|
title: 'Addition Tool',
|
|
70
74
|
description: 'Add two numbers',
|
|
71
|
-
inputSchema: { a: z.number(), b: z.number() }
|
|
75
|
+
inputSchema: { a: z.number(), b: z.number() },
|
|
76
|
+
outputSchema: { result: z.number() }
|
|
72
77
|
},
|
|
73
|
-
async ({ a, b }) =>
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
async ({ a, b }) => {
|
|
79
|
+
const output = { result: a + b };
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
82
|
+
structuredContent: output
|
|
83
|
+
};
|
|
84
|
+
}
|
|
76
85
|
);
|
|
77
86
|
|
|
78
87
|
// Add a dynamic greeting resource
|
|
@@ -93,19 +102,44 @@ server.registerResource(
|
|
|
93
102
|
})
|
|
94
103
|
);
|
|
95
104
|
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
105
|
+
// Set up Express and HTTP transport
|
|
106
|
+
const app = express();
|
|
107
|
+
app.use(express.json());
|
|
108
|
+
|
|
109
|
+
app.post('/mcp', async (req, res) => {
|
|
110
|
+
// Create a new transport for each request to prevent request ID collisions
|
|
111
|
+
const transport = new StreamableHTTPServerTransport({
|
|
112
|
+
sessionIdGenerator: undefined,
|
|
113
|
+
enableJsonResponse: true
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
res.on('close', () => {
|
|
117
|
+
transport.close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await server.connect(transport);
|
|
121
|
+
await transport.handleRequest(req, res, req.body);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
125
|
+
app.listen(port, () => {
|
|
126
|
+
console.log(`Demo MCP Server running on http://localhost:${port}/mcp`);
|
|
127
|
+
}).on('error', error => {
|
|
128
|
+
console.error('Server error:', error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
99
131
|
```
|
|
100
132
|
|
|
101
|
-
|
|
133
|
+
Install the deps with `npm install @modelcontextprotocol/sdk express zod@3`, and run with `npx -y tsx server.ts`.
|
|
102
134
|
|
|
103
|
-
|
|
135
|
+
You can connect to it using any MCP client that supports streamable http, such as:
|
|
104
136
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
137
|
+
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `npx @modelcontextprotocol/inspector` and connect to the streamable HTTP URL `http://localhost:3000/mcp`
|
|
138
|
+
- [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp): `claude mcp add --transport http my-server http://localhost:3000/mcp`
|
|
139
|
+
- [VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers): `code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"`
|
|
140
|
+
- [Cursor](https://cursor.com/docs/context/mcp): Click [this deeplink](cursor://anysphere.cursor-deeplink/mcp/install?name=my-server&config=eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWNwIn0%3D)
|
|
141
|
+
|
|
142
|
+
Then try asking your agent to add two numbers using its new tool!
|
|
109
143
|
|
|
110
144
|
## Core Concepts
|
|
111
145
|
|
|
@@ -120,9 +154,112 @@ const server = new McpServer({
|
|
|
120
154
|
});
|
|
121
155
|
```
|
|
122
156
|
|
|
157
|
+
### Tools
|
|
158
|
+
|
|
159
|
+
[Tools](https://modelcontextprotocol.io/specification/latest/server/tools) let LLMs take actions through your server. Tools can perform computation, fetch data and have side effects. Tools should be designed to be model-controlled - i.e. AI models will decide which tools to call,
|
|
160
|
+
and the arguments.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Simple tool with parameters
|
|
164
|
+
server.registerTool(
|
|
165
|
+
'calculate-bmi',
|
|
166
|
+
{
|
|
167
|
+
title: 'BMI Calculator',
|
|
168
|
+
description: 'Calculate Body Mass Index',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
weightKg: z.number(),
|
|
171
|
+
heightM: z.number()
|
|
172
|
+
},
|
|
173
|
+
outputSchema: { bmi: z.number() }
|
|
174
|
+
},
|
|
175
|
+
async ({ weightKg, heightM }) => {
|
|
176
|
+
const output = { bmi: weightKg / (heightM * heightM) };
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: JSON.stringify(output)
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
structuredContent: output
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Async tool with external API call
|
|
190
|
+
server.registerTool(
|
|
191
|
+
'fetch-weather',
|
|
192
|
+
{
|
|
193
|
+
title: 'Weather Fetcher',
|
|
194
|
+
description: 'Get weather data for a city',
|
|
195
|
+
inputSchema: { city: z.string() },
|
|
196
|
+
outputSchema: { temperature: z.number(), conditions: z.string() }
|
|
197
|
+
},
|
|
198
|
+
async ({ city }) => {
|
|
199
|
+
const response = await fetch(`https://api.weather.com/${city}`);
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
const output = { temperature: data.temp, conditions: data.conditions };
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
204
|
+
structuredContent: output
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Tool that returns ResourceLinks
|
|
210
|
+
server.registerTool(
|
|
211
|
+
'list-files',
|
|
212
|
+
{
|
|
213
|
+
title: 'List Files',
|
|
214
|
+
description: 'List project files',
|
|
215
|
+
inputSchema: { pattern: z.string() },
|
|
216
|
+
outputSchema: {
|
|
217
|
+
count: z.number(),
|
|
218
|
+
files: z.array(z.object({ name: z.string(), uri: z.string() }))
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async ({ pattern }) => {
|
|
222
|
+
const output = {
|
|
223
|
+
count: 2,
|
|
224
|
+
files: [
|
|
225
|
+
{ name: 'README.md', uri: 'file:///project/README.md' },
|
|
226
|
+
{ name: 'index.ts', uri: 'file:///project/src/index.ts' }
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{ type: 'text', text: JSON.stringify(output) },
|
|
232
|
+
// ResourceLinks let tools return references without file content
|
|
233
|
+
{
|
|
234
|
+
type: 'resource_link',
|
|
235
|
+
uri: 'file:///project/README.md',
|
|
236
|
+
name: 'README.md',
|
|
237
|
+
mimeType: 'text/markdown',
|
|
238
|
+
description: 'A README file'
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: 'resource_link',
|
|
242
|
+
uri: 'file:///project/src/index.ts',
|
|
243
|
+
name: 'index.ts',
|
|
244
|
+
mimeType: 'text/typescript',
|
|
245
|
+
description: 'An index file'
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
structuredContent: output
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### ResourceLinks
|
|
255
|
+
|
|
256
|
+
Tools can return `ResourceLink` objects to reference resources without embedding their full content. This can be helpful for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.
|
|
257
|
+
|
|
123
258
|
### Resources
|
|
124
259
|
|
|
125
|
-
Resources
|
|
260
|
+
[Resources](https://modelcontextprotocol.io/specification/latest/server/resources) can also expose data to LLMs, but unlike tools shouldn't perform significant computation or have side effects.
|
|
261
|
+
|
|
262
|
+
Resources are designed to be used in an application-driven way, meaning MCP client applications can decide how to expose them. For example, a client could expose a resource picker to the human, or could expose them to the model directly.
|
|
126
263
|
|
|
127
264
|
```typescript
|
|
128
265
|
// Static resource
|
|
@@ -192,87 +329,9 @@ server.registerResource(
|
|
|
192
329
|
);
|
|
193
330
|
```
|
|
194
331
|
|
|
195
|
-
### Tools
|
|
196
|
-
|
|
197
|
-
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
// Simple tool with parameters
|
|
201
|
-
server.registerTool(
|
|
202
|
-
'calculate-bmi',
|
|
203
|
-
{
|
|
204
|
-
title: 'BMI Calculator',
|
|
205
|
-
description: 'Calculate Body Mass Index',
|
|
206
|
-
inputSchema: {
|
|
207
|
-
weightKg: z.number(),
|
|
208
|
-
heightM: z.number()
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
async ({ weightKg, heightM }) => ({
|
|
212
|
-
content: [
|
|
213
|
-
{
|
|
214
|
-
type: 'text',
|
|
215
|
-
text: String(weightKg / (heightM * heightM))
|
|
216
|
-
}
|
|
217
|
-
]
|
|
218
|
-
})
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// Async tool with external API call
|
|
222
|
-
server.registerTool(
|
|
223
|
-
'fetch-weather',
|
|
224
|
-
{
|
|
225
|
-
title: 'Weather Fetcher',
|
|
226
|
-
description: 'Get weather data for a city',
|
|
227
|
-
inputSchema: { city: z.string() }
|
|
228
|
-
},
|
|
229
|
-
async ({ city }) => {
|
|
230
|
-
const response = await fetch(`https://api.weather.com/${city}`);
|
|
231
|
-
const data = await response.text();
|
|
232
|
-
return {
|
|
233
|
-
content: [{ type: 'text', text: data }]
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
// Tool that returns ResourceLinks
|
|
239
|
-
server.registerTool(
|
|
240
|
-
'list-files',
|
|
241
|
-
{
|
|
242
|
-
title: 'List Files',
|
|
243
|
-
description: 'List project files',
|
|
244
|
-
inputSchema: { pattern: z.string() }
|
|
245
|
-
},
|
|
246
|
-
async ({ pattern }) => ({
|
|
247
|
-
content: [
|
|
248
|
-
{ type: 'text', text: `Found files matching "${pattern}":` },
|
|
249
|
-
// ResourceLinks let tools return references without file content
|
|
250
|
-
{
|
|
251
|
-
type: 'resource_link',
|
|
252
|
-
uri: 'file:///project/README.md',
|
|
253
|
-
name: 'README.md',
|
|
254
|
-
mimeType: 'text/markdown',
|
|
255
|
-
description: 'A README file'
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
type: 'resource_link',
|
|
259
|
-
uri: 'file:///project/src/index.ts',
|
|
260
|
-
name: 'index.ts',
|
|
261
|
-
mimeType: 'text/typescript',
|
|
262
|
-
description: 'An index file'
|
|
263
|
-
}
|
|
264
|
-
]
|
|
265
|
-
})
|
|
266
|
-
);
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
#### ResourceLinks
|
|
270
|
-
|
|
271
|
-
Tools can return `ResourceLink` objects to reference resources without embedding their full content. This is essential for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.
|
|
272
|
-
|
|
273
332
|
### Prompts
|
|
274
333
|
|
|
275
|
-
Prompts are reusable templates that help
|
|
334
|
+
[Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts) are reusable templates that help humans prompt models to interact with your server. They're designed to be user-driven, and might appear as slash commands in a chat interface.
|
|
276
335
|
|
|
277
336
|
```typescript
|
|
278
337
|
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
|
@@ -364,7 +423,7 @@ const result = await client.complete({
|
|
|
364
423
|
|
|
365
424
|
### Display Names and Metadata
|
|
366
425
|
|
|
367
|
-
All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name, while `name` remains the unique identifier.
|
|
426
|
+
All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name (e.g. 'Create a new issue'), while `name` remains the unique identifier (e.g. `create_issue`).
|
|
368
427
|
|
|
369
428
|
**Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility.
|
|
370
429
|
|
|
@@ -416,7 +475,8 @@ MCP servers can request LLM completions from connected clients that support samp
|
|
|
416
475
|
|
|
417
476
|
```typescript
|
|
418
477
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
419
|
-
import {
|
|
478
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
479
|
+
import express from 'express';
|
|
420
480
|
import { z } from 'zod';
|
|
421
481
|
|
|
422
482
|
const mcpServer = new McpServer({
|
|
@@ -428,10 +488,12 @@ const mcpServer = new McpServer({
|
|
|
428
488
|
mcpServer.registerTool(
|
|
429
489
|
'summarize',
|
|
430
490
|
{
|
|
491
|
+
title: 'Text Summarizer',
|
|
431
492
|
description: 'Summarize any text using an LLM',
|
|
432
493
|
inputSchema: {
|
|
433
494
|
text: z.string().describe('Text to summarize')
|
|
434
|
-
}
|
|
495
|
+
},
|
|
496
|
+
outputSchema: { summary: z.string() }
|
|
435
497
|
},
|
|
436
498
|
async ({ text }) => {
|
|
437
499
|
// Call the LLM through MCP sampling
|
|
@@ -448,24 +510,36 @@ mcpServer.registerTool(
|
|
|
448
510
|
maxTokens: 500
|
|
449
511
|
});
|
|
450
512
|
|
|
513
|
+
const summary = response.content.type === 'text' ? response.content.text : 'Unable to generate summary';
|
|
514
|
+
const output = { summary };
|
|
451
515
|
return {
|
|
452
|
-
content: [
|
|
453
|
-
|
|
454
|
-
type: 'text',
|
|
455
|
-
text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary'
|
|
456
|
-
}
|
|
457
|
-
]
|
|
516
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
517
|
+
structuredContent: output
|
|
458
518
|
};
|
|
459
519
|
}
|
|
460
520
|
);
|
|
461
521
|
|
|
462
|
-
|
|
463
|
-
|
|
522
|
+
const app = express();
|
|
523
|
+
app.use(express.json());
|
|
524
|
+
|
|
525
|
+
app.post('/mcp', async (req, res) => {
|
|
526
|
+
const transport = new StreamableHTTPServerTransport({
|
|
527
|
+
sessionIdGenerator: undefined,
|
|
528
|
+
enableJsonResponse: true
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
res.on('close', () => {
|
|
532
|
+
transport.close();
|
|
533
|
+
});
|
|
534
|
+
|
|
464
535
|
await mcpServer.connect(transport);
|
|
465
|
-
|
|
466
|
-
}
|
|
536
|
+
await transport.handleRequest(req, res, req.body);
|
|
537
|
+
});
|
|
467
538
|
|
|
468
|
-
|
|
539
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
540
|
+
app.listen(port, () => {
|
|
541
|
+
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
|
542
|
+
}).on('error', error => {
|
|
469
543
|
console.error('Server error:', error);
|
|
470
544
|
process.exit(1);
|
|
471
545
|
});
|
|
@@ -475,32 +549,92 @@ main().catch(error => {
|
|
|
475
549
|
|
|
476
550
|
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:
|
|
477
551
|
|
|
478
|
-
###
|
|
552
|
+
### Streamable HTTP
|
|
553
|
+
|
|
554
|
+
For remote servers, use the Streamable HTTP transport.
|
|
555
|
+
|
|
556
|
+
#### Without Session Management (Recommended)
|
|
479
557
|
|
|
480
|
-
For
|
|
558
|
+
For most use cases where session management isn't needed:
|
|
481
559
|
|
|
482
560
|
```typescript
|
|
483
561
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
484
|
-
import {
|
|
562
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
563
|
+
import express from 'express';
|
|
564
|
+
import { z } from 'zod';
|
|
485
565
|
|
|
566
|
+
const app = express();
|
|
567
|
+
app.use(express.json());
|
|
568
|
+
|
|
569
|
+
// Create the MCP server once (can be reused across requests)
|
|
486
570
|
const server = new McpServer({
|
|
487
571
|
name: 'example-server',
|
|
488
572
|
version: '1.0.0'
|
|
489
573
|
});
|
|
490
574
|
|
|
491
|
-
//
|
|
575
|
+
// Set up your tools, resources, and prompts
|
|
576
|
+
server.registerTool(
|
|
577
|
+
'echo',
|
|
578
|
+
{
|
|
579
|
+
title: 'Echo Tool',
|
|
580
|
+
description: 'Echoes back the provided message',
|
|
581
|
+
inputSchema: { message: z.string() },
|
|
582
|
+
outputSchema: { echo: z.string() }
|
|
583
|
+
},
|
|
584
|
+
async ({ message }) => {
|
|
585
|
+
const output = { echo: `Tool echo: ${message}` };
|
|
586
|
+
return {
|
|
587
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
588
|
+
structuredContent: output
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
);
|
|
492
592
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
593
|
+
app.post('/mcp', async (req, res) => {
|
|
594
|
+
// In stateless mode, create a new transport for each request to prevent
|
|
595
|
+
// request ID collisions. Different clients may use the same JSON-RPC request IDs,
|
|
596
|
+
// which would cause responses to be routed to the wrong HTTP connections if
|
|
597
|
+
// the transport state is shared.
|
|
496
598
|
|
|
497
|
-
|
|
599
|
+
try {
|
|
600
|
+
const transport = new StreamableHTTPServerTransport({
|
|
601
|
+
sessionIdGenerator: undefined,
|
|
602
|
+
enableJsonResponse: true
|
|
603
|
+
});
|
|
498
604
|
|
|
499
|
-
|
|
605
|
+
res.on('close', () => {
|
|
606
|
+
transport.close();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
await server.connect(transport);
|
|
610
|
+
await transport.handleRequest(req, res, req.body);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.error('Error handling MCP request:', error);
|
|
613
|
+
if (!res.headersSent) {
|
|
614
|
+
res.status(500).json({
|
|
615
|
+
jsonrpc: '2.0',
|
|
616
|
+
error: {
|
|
617
|
+
code: -32603,
|
|
618
|
+
message: 'Internal server error'
|
|
619
|
+
},
|
|
620
|
+
id: null
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
627
|
+
app.listen(port, () => {
|
|
628
|
+
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
|
629
|
+
}).on('error', error => {
|
|
630
|
+
console.error('Server error:', error);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
});
|
|
633
|
+
```
|
|
500
634
|
|
|
501
635
|
#### With Session Management
|
|
502
636
|
|
|
503
|
-
In some cases, servers need
|
|
637
|
+
In some cases, servers need stateful sessions. This can be achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management) in the MCP protocol.
|
|
504
638
|
|
|
505
639
|
```typescript
|
|
506
640
|
import express from 'express';
|
|
@@ -591,8 +725,6 @@ app.delete('/mcp', handleSessionRequest);
|
|
|
591
725
|
app.listen(3000);
|
|
592
726
|
```
|
|
593
727
|
|
|
594
|
-
> [!TIP] When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. Read the following section for examples.
|
|
595
|
-
|
|
596
728
|
#### CORS Configuration for Browser-Based Clients
|
|
597
729
|
|
|
598
730
|
If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it:
|
|
@@ -617,100 +749,6 @@ This configuration is necessary because:
|
|
|
617
749
|
- Browsers restrict access to response headers unless explicitly exposed via CORS
|
|
618
750
|
- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses
|
|
619
751
|
|
|
620
|
-
#### Without Session Management (Stateless)
|
|
621
|
-
|
|
622
|
-
For simpler use cases where session management isn't needed:
|
|
623
|
-
|
|
624
|
-
```typescript
|
|
625
|
-
const app = express();
|
|
626
|
-
app.use(express.json());
|
|
627
|
-
|
|
628
|
-
app.post('/mcp', async (req: Request, res: Response) => {
|
|
629
|
-
// In stateless mode, create a new instance of transport and server for each request
|
|
630
|
-
// to ensure complete isolation. A single instance would cause request ID collisions
|
|
631
|
-
// when multiple clients connect concurrently.
|
|
632
|
-
|
|
633
|
-
try {
|
|
634
|
-
const server = getServer();
|
|
635
|
-
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
|
636
|
-
sessionIdGenerator: undefined
|
|
637
|
-
});
|
|
638
|
-
res.on('close', () => {
|
|
639
|
-
console.log('Request closed');
|
|
640
|
-
transport.close();
|
|
641
|
-
server.close();
|
|
642
|
-
});
|
|
643
|
-
await server.connect(transport);
|
|
644
|
-
await transport.handleRequest(req, res, req.body);
|
|
645
|
-
} catch (error) {
|
|
646
|
-
console.error('Error handling MCP request:', error);
|
|
647
|
-
if (!res.headersSent) {
|
|
648
|
-
res.status(500).json({
|
|
649
|
-
jsonrpc: '2.0',
|
|
650
|
-
error: {
|
|
651
|
-
code: -32603,
|
|
652
|
-
message: 'Internal server error'
|
|
653
|
-
},
|
|
654
|
-
id: null
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// SSE notifications not supported in stateless mode
|
|
661
|
-
app.get('/mcp', async (req: Request, res: Response) => {
|
|
662
|
-
console.log('Received GET MCP request');
|
|
663
|
-
res.writeHead(405).end(
|
|
664
|
-
JSON.stringify({
|
|
665
|
-
jsonrpc: '2.0',
|
|
666
|
-
error: {
|
|
667
|
-
code: -32000,
|
|
668
|
-
message: 'Method not allowed.'
|
|
669
|
-
},
|
|
670
|
-
id: null
|
|
671
|
-
})
|
|
672
|
-
);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
// Session termination not needed in stateless mode
|
|
676
|
-
app.delete('/mcp', async (req: Request, res: Response) => {
|
|
677
|
-
console.log('Received DELETE MCP request');
|
|
678
|
-
res.writeHead(405).end(
|
|
679
|
-
JSON.stringify({
|
|
680
|
-
jsonrpc: '2.0',
|
|
681
|
-
error: {
|
|
682
|
-
code: -32000,
|
|
683
|
-
message: 'Method not allowed.'
|
|
684
|
-
},
|
|
685
|
-
id: null
|
|
686
|
-
})
|
|
687
|
-
);
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
// Start the server
|
|
691
|
-
const PORT = 3000;
|
|
692
|
-
setupServer()
|
|
693
|
-
.then(() => {
|
|
694
|
-
app.listen(PORT, error => {
|
|
695
|
-
if (error) {
|
|
696
|
-
console.error('Failed to start server:', error);
|
|
697
|
-
process.exit(1);
|
|
698
|
-
}
|
|
699
|
-
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
|
|
700
|
-
});
|
|
701
|
-
})
|
|
702
|
-
.catch(error => {
|
|
703
|
-
console.error('Failed to set up the server:', error);
|
|
704
|
-
process.exit(1);
|
|
705
|
-
});
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
This stateless approach is useful for:
|
|
709
|
-
|
|
710
|
-
- Simple API wrappers
|
|
711
|
-
- RESTful scenarios where each request is independent
|
|
712
|
-
- Horizontally scaled deployments without shared session state
|
|
713
|
-
|
|
714
752
|
#### DNS Rebinding Protection
|
|
715
753
|
|
|
716
754
|
The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection is **disabled** for backwards compatibility.
|
|
@@ -727,6 +765,25 @@ const transport = new StreamableHTTPServerTransport({
|
|
|
727
765
|
});
|
|
728
766
|
```
|
|
729
767
|
|
|
768
|
+
### stdio
|
|
769
|
+
|
|
770
|
+
For local integrations spawned by another process, you can use the stdio transport:
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
774
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
775
|
+
|
|
776
|
+
const server = new McpServer({
|
|
777
|
+
name: 'example-server',
|
|
778
|
+
version: '1.0.0'
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// ... set up server resources, tools, and prompts ...
|
|
782
|
+
|
|
783
|
+
const transport = new StdioServerTransport();
|
|
784
|
+
await server.connect(transport);
|
|
785
|
+
```
|
|
786
|
+
|
|
730
787
|
### Testing and Debugging
|
|
731
788
|
|
|
732
789
|
To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
|
|
@@ -746,6 +803,23 @@ const server = new McpServer({
|
|
|
746
803
|
version: '1.0.0'
|
|
747
804
|
});
|
|
748
805
|
|
|
806
|
+
server.registerTool(
|
|
807
|
+
'echo',
|
|
808
|
+
{
|
|
809
|
+
title: 'Echo Tool',
|
|
810
|
+
description: 'Echoes back the provided message',
|
|
811
|
+
inputSchema: { message: z.string() },
|
|
812
|
+
outputSchema: { echo: z.string() }
|
|
813
|
+
},
|
|
814
|
+
async ({ message }) => {
|
|
815
|
+
const output = { echo: `Tool echo: ${message}` };
|
|
816
|
+
return {
|
|
817
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
818
|
+
structuredContent: output
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
|
|
749
823
|
server.registerResource(
|
|
750
824
|
'echo',
|
|
751
825
|
new ResourceTemplate('echo://{message}', { list: undefined }),
|
|
@@ -763,18 +837,6 @@ server.registerResource(
|
|
|
763
837
|
})
|
|
764
838
|
);
|
|
765
839
|
|
|
766
|
-
server.registerTool(
|
|
767
|
-
'echo',
|
|
768
|
-
{
|
|
769
|
-
title: 'Echo Tool',
|
|
770
|
-
description: 'Echoes back the provided message',
|
|
771
|
-
inputSchema: { message: z.string() }
|
|
772
|
-
},
|
|
773
|
-
async ({ message }) => ({
|
|
774
|
-
content: [{ type: 'text', text: `Tool echo: ${message}` }]
|
|
775
|
-
})
|
|
776
|
-
);
|
|
777
|
-
|
|
778
840
|
server.registerPrompt(
|
|
779
841
|
'echo',
|
|
780
842
|
{
|
|
@@ -851,19 +913,25 @@ server.registerTool(
|
|
|
851
913
|
{
|
|
852
914
|
title: 'SQL Query',
|
|
853
915
|
description: 'Execute SQL queries on the database',
|
|
854
|
-
inputSchema: { sql: z.string() }
|
|
916
|
+
inputSchema: { sql: z.string() },
|
|
917
|
+
outputSchema: {
|
|
918
|
+
rows: z.array(z.record(z.any())),
|
|
919
|
+
rowCount: z.number()
|
|
920
|
+
}
|
|
855
921
|
},
|
|
856
922
|
async ({ sql }) => {
|
|
857
923
|
const db = getDb();
|
|
858
924
|
try {
|
|
859
925
|
const results = await db.all(sql);
|
|
926
|
+
const output = { rows: results, rowCount: results.length };
|
|
860
927
|
return {
|
|
861
928
|
content: [
|
|
862
929
|
{
|
|
863
930
|
type: 'text',
|
|
864
|
-
text: JSON.stringify(
|
|
931
|
+
text: JSON.stringify(output, null, 2)
|
|
865
932
|
}
|
|
866
|
-
]
|
|
933
|
+
],
|
|
934
|
+
structuredContent: output
|
|
867
935
|
};
|
|
868
936
|
} catch (err: unknown) {
|
|
869
937
|
const error = err as Error;
|
|
@@ -889,8 +957,10 @@ server.registerTool(
|
|
|
889
957
|
|
|
890
958
|
If you want to offer an initial set of tools/prompts/resources, but later add additional ones based on user action or external state change, you can add/update/remove them _after_ the Server is connected. This will automatically emit the corresponding `listChanged` notifications:
|
|
891
959
|
|
|
892
|
-
```
|
|
960
|
+
```typescript
|
|
893
961
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
962
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
963
|
+
import express from 'express';
|
|
894
964
|
import { z } from 'zod';
|
|
895
965
|
|
|
896
966
|
const server = new McpServer({
|
|
@@ -898,23 +968,64 @@ const server = new McpServer({
|
|
|
898
968
|
version: '1.0.0'
|
|
899
969
|
});
|
|
900
970
|
|
|
901
|
-
const listMessageTool = server.
|
|
902
|
-
|
|
903
|
-
|
|
971
|
+
const listMessageTool = server.registerTool(
|
|
972
|
+
'listMessages',
|
|
973
|
+
{
|
|
974
|
+
title: 'List Messages',
|
|
975
|
+
description: 'List messages in a channel',
|
|
976
|
+
inputSchema: { channel: z.string() },
|
|
977
|
+
outputSchema: { messages: z.array(z.string()) }
|
|
978
|
+
},
|
|
979
|
+
async ({ channel }) => {
|
|
980
|
+
const messages = await listMessages(channel);
|
|
981
|
+
const output = { messages };
|
|
982
|
+
return {
|
|
983
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
984
|
+
structuredContent: output
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
);
|
|
904
988
|
|
|
905
|
-
const putMessageTool = server.
|
|
906
|
-
|
|
907
|
-
|
|
989
|
+
const putMessageTool = server.registerTool(
|
|
990
|
+
'putMessage',
|
|
991
|
+
{
|
|
992
|
+
title: 'Put Message',
|
|
993
|
+
description: 'Send a message to a channel',
|
|
994
|
+
inputSchema: { channel: z.string(), message: z.string() },
|
|
995
|
+
outputSchema: { success: z.boolean() }
|
|
996
|
+
},
|
|
997
|
+
async ({ channel, message }) => {
|
|
998
|
+
await putMessage(channel, message);
|
|
999
|
+
const output = { success: true };
|
|
1000
|
+
return {
|
|
1001
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
1002
|
+
structuredContent: output
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
908
1006
|
// Until we upgrade auth, `putMessage` is disabled (won't show up in listTools)
|
|
909
1007
|
putMessageTool.disable();
|
|
910
1008
|
|
|
911
|
-
const upgradeAuthTool = server.
|
|
1009
|
+
const upgradeAuthTool = server.registerTool(
|
|
912
1010
|
'upgradeAuth',
|
|
913
|
-
{
|
|
1011
|
+
{
|
|
1012
|
+
title: 'Upgrade Authorization',
|
|
1013
|
+
description: 'Upgrade user authorization level',
|
|
1014
|
+
inputSchema: { permission: z.enum(['write', 'admin']) },
|
|
1015
|
+
outputSchema: {
|
|
1016
|
+
success: z.boolean(),
|
|
1017
|
+
newPermission: z.string()
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
914
1020
|
// Any mutations here will automatically emit `listChanged` notifications
|
|
915
1021
|
async ({ permission }) => {
|
|
916
1022
|
const { ok, err, previous } = await upgradeAuthAndStoreToken(permission);
|
|
917
|
-
if (!ok)
|
|
1023
|
+
if (!ok) {
|
|
1024
|
+
return {
|
|
1025
|
+
content: [{ type: 'text', text: `Error: ${err}` }],
|
|
1026
|
+
isError: true
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
918
1029
|
|
|
919
1030
|
// If we previously had read-only access, 'putMessage' is now available
|
|
920
1031
|
if (previous === 'read') {
|
|
@@ -931,12 +1042,37 @@ const upgradeAuthTool = server.tool(
|
|
|
931
1042
|
// If we're now an admin, we no longer have anywhere to upgrade to, so fully remove that tool
|
|
932
1043
|
upgradeAuthTool.remove();
|
|
933
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
const output = { success: true, newPermission: permission };
|
|
1047
|
+
return {
|
|
1048
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
1049
|
+
structuredContent: output
|
|
1050
|
+
};
|
|
934
1051
|
}
|
|
935
1052
|
);
|
|
936
1053
|
|
|
937
|
-
// Connect
|
|
938
|
-
const
|
|
939
|
-
|
|
1054
|
+
// Connect with HTTP transport
|
|
1055
|
+
const app = express();
|
|
1056
|
+
app.use(express.json());
|
|
1057
|
+
|
|
1058
|
+
app.post('/mcp', async (req, res) => {
|
|
1059
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1060
|
+
sessionIdGenerator: undefined,
|
|
1061
|
+
enableJsonResponse: true
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
res.on('close', () => {
|
|
1065
|
+
transport.close();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await server.connect(transport);
|
|
1069
|
+
await transport.handleRequest(req, res, req.body);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
1073
|
+
app.listen(port, () => {
|
|
1074
|
+
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
|
1075
|
+
});
|
|
940
1076
|
```
|
|
941
1077
|
|
|
942
1078
|
### Improving Network Efficiency with Notification Debouncing
|
|
@@ -1043,12 +1179,27 @@ MCP servers can request additional information from users through the elicitatio
|
|
|
1043
1179
|
|
|
1044
1180
|
```typescript
|
|
1045
1181
|
// Server-side: Restaurant booking tool that asks for alternatives
|
|
1046
|
-
server.
|
|
1182
|
+
server.registerTool(
|
|
1047
1183
|
'book-restaurant',
|
|
1048
1184
|
{
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1185
|
+
title: 'Book Restaurant',
|
|
1186
|
+
description: 'Book a table at a restaurant',
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
restaurant: z.string(),
|
|
1189
|
+
date: z.string(),
|
|
1190
|
+
partySize: z.number()
|
|
1191
|
+
},
|
|
1192
|
+
outputSchema: {
|
|
1193
|
+
success: z.boolean(),
|
|
1194
|
+
booking: z
|
|
1195
|
+
.object({
|
|
1196
|
+
restaurant: z.string(),
|
|
1197
|
+
date: z.string(),
|
|
1198
|
+
partySize: z.number()
|
|
1199
|
+
})
|
|
1200
|
+
.optional(),
|
|
1201
|
+
alternatives: z.array(z.string()).optional()
|
|
1202
|
+
}
|
|
1052
1203
|
},
|
|
1053
1204
|
async ({ restaurant, date, partySize }) => {
|
|
1054
1205
|
// Check availability
|
|
@@ -1080,35 +1231,44 @@ server.tool(
|
|
|
1080
1231
|
|
|
1081
1232
|
if (result.action === 'accept' && result.content?.checkAlternatives) {
|
|
1082
1233
|
const alternatives = await findAlternatives(restaurant, date, partySize, result.content.flexibleDates as string);
|
|
1234
|
+
const output = { success: false, alternatives };
|
|
1083
1235
|
return {
|
|
1084
1236
|
content: [
|
|
1085
1237
|
{
|
|
1086
1238
|
type: 'text',
|
|
1087
|
-
text:
|
|
1239
|
+
text: JSON.stringify(output)
|
|
1088
1240
|
}
|
|
1089
|
-
]
|
|
1241
|
+
],
|
|
1242
|
+
structuredContent: output
|
|
1090
1243
|
};
|
|
1091
1244
|
}
|
|
1092
1245
|
|
|
1246
|
+
const output = { success: false };
|
|
1093
1247
|
return {
|
|
1094
1248
|
content: [
|
|
1095
1249
|
{
|
|
1096
1250
|
type: 'text',
|
|
1097
|
-
text:
|
|
1251
|
+
text: JSON.stringify(output)
|
|
1098
1252
|
}
|
|
1099
|
-
]
|
|
1253
|
+
],
|
|
1254
|
+
structuredContent: output
|
|
1100
1255
|
};
|
|
1101
1256
|
}
|
|
1102
1257
|
|
|
1103
1258
|
// Book the table
|
|
1104
1259
|
await makeBooking(restaurant, date, partySize);
|
|
1260
|
+
const output = {
|
|
1261
|
+
success: true,
|
|
1262
|
+
booking: { restaurant, date, partySize }
|
|
1263
|
+
};
|
|
1105
1264
|
return {
|
|
1106
1265
|
content: [
|
|
1107
1266
|
{
|
|
1108
1267
|
type: 'text',
|
|
1109
|
-
text:
|
|
1268
|
+
text: JSON.stringify(output)
|
|
1110
1269
|
}
|
|
1111
|
-
]
|
|
1270
|
+
],
|
|
1271
|
+
structuredContent: output
|
|
1112
1272
|
};
|
|
1113
1273
|
}
|
|
1114
1274
|
);
|