docs: improve main README with better quick start, include examples of stateless HTTP, explain tools v resources v prompts (#980)
This commit is contained in:
parent
29b65b0ce7
commit
e0de082901
1 changed files with 417 additions and 259 deletions
680
README.md
680
README.md
|
|
@ -1,6 +1,7 @@
|
|||
# MCP TypeScript SDK  
|
||||
|
||||
## Table of Contents
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Installation](#installation)
|
||||
|
|
@ -30,14 +31,16 @@
|
|||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
## Overview
|
||||
|
||||
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 the full MCP specification, making it easy to:
|
||||
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
|
||||
[the full MCP specification](https://modelcontextprotocol.io/specification/latest), making it easy to:
|
||||
|
||||
- Build MCP clients that can connect to any MCP server
|
||||
- Create MCP servers that expose resources, prompts and tools
|
||||
- Build MCP clients that can connect to any MCP server
|
||||
- Use standard transports like stdio and Streamable HTTP
|
||||
- Handle all MCP protocol messages and lifecycle events
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -45,15 +48,14 @@ The Model Context Protocol allows applications to provide context for LLMs in a
|
|||
npm install @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
> ⚠️ MCP requires Node.js v18.x or higher to work fine.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Let's create a simple MCP server that exposes a calculator tool and some data:
|
||||
Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as `server.ts`:
|
||||
|
||||
```typescript
|
||||
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Create an MCP server
|
||||
|
|
@ -68,11 +70,16 @@ server.registerTool(
|
|||
{
|
||||
title: 'Addition Tool',
|
||||
description: 'Add two numbers',
|
||||
inputSchema: { a: z.number(), b: z.number() }
|
||||
inputSchema: { a: z.number(), b: z.number() },
|
||||
outputSchema: { result: z.number() }
|
||||
},
|
||||
async ({ a, b }) => ({
|
||||
content: [{ type: 'text', text: String(a + b) }]
|
||||
})
|
||||
async ({ a, b }) => {
|
||||
const output = { result: a + b };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Add a dynamic greeting resource
|
||||
|
|
@ -93,19 +100,44 @@ server.registerResource(
|
|||
})
|
||||
);
|
||||
|
||||
// Start receiving messages on stdin and sending messages on stdout
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// Set up Express and HTTP transport
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/mcp', async (req, res) => {
|
||||
// Create a new transport for each request to prevent request ID collisions
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
});
|
||||
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
app.listen(port, () => {
|
||||
console.log(`Demo MCP Server running on http://localhost:${port}/mcp`);
|
||||
}).on('error', error => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## What is MCP?
|
||||
Install the deps with `npm install @modelcontextprotocol/sdk express zod@3`, and run with `npx -y tsx server.ts`.
|
||||
|
||||
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
|
||||
You can connect to it using any MCP client that supports streamable http, such as:
|
||||
|
||||
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
|
||||
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
|
||||
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
|
||||
- And more!
|
||||
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `npx @modelcontextprotocol/inspector` and connect to the streamable HTTP URL `http://localhost:3000/mcp`
|
||||
- [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp): `claude mcp add --transport http my-server http://localhost:3000/mcp`
|
||||
- [VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers): `code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"`
|
||||
- [Cursor](https://cursor.com/docs/context/mcp): Click [this deeplink](cursor://anysphere.cursor-deeplink/mcp/install?name=my-server&config=eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWNwIn0%3D)
|
||||
|
||||
Then try asking your agent to add two numbers using its new tool!
|
||||
|
||||
## Core Concepts
|
||||
|
||||
|
|
@ -120,9 +152,112 @@ const server = new McpServer({
|
|||
});
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
[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,
|
||||
and the arguments.
|
||||
|
||||
```typescript
|
||||
// Simple tool with parameters
|
||||
server.registerTool(
|
||||
'calculate-bmi',
|
||||
{
|
||||
title: 'BMI Calculator',
|
||||
description: 'Calculate Body Mass Index',
|
||||
inputSchema: {
|
||||
weightKg: z.number(),
|
||||
heightM: z.number()
|
||||
},
|
||||
outputSchema: { bmi: z.number() }
|
||||
},
|
||||
async ({ weightKg, heightM }) => {
|
||||
const output = { bmi: weightKg / (heightM * heightM) };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(output)
|
||||
}
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Async tool with external API call
|
||||
server.registerTool(
|
||||
'fetch-weather',
|
||||
{
|
||||
title: 'Weather Fetcher',
|
||||
description: 'Get weather data for a city',
|
||||
inputSchema: { city: z.string() },
|
||||
outputSchema: { temperature: z.number(), conditions: z.string() }
|
||||
},
|
||||
async ({ city }) => {
|
||||
const response = await fetch(`https://api.weather.com/${city}`);
|
||||
const data = await response.json();
|
||||
const output = { temperature: data.temp, conditions: data.conditions };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Tool that returns ResourceLinks
|
||||
server.registerTool(
|
||||
'list-files',
|
||||
{
|
||||
title: 'List Files',
|
||||
description: 'List project files',
|
||||
inputSchema: { pattern: z.string() },
|
||||
outputSchema: {
|
||||
count: z.number(),
|
||||
files: z.array(z.object({ name: z.string(), uri: z.string() }))
|
||||
}
|
||||
},
|
||||
async ({ pattern }) => {
|
||||
const output = {
|
||||
count: 2,
|
||||
files: [
|
||||
{ name: 'README.md', uri: 'file:///project/README.md' },
|
||||
{ name: 'index.ts', uri: 'file:///project/src/index.ts' }
|
||||
]
|
||||
};
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: JSON.stringify(output) },
|
||||
// ResourceLinks let tools return references without file content
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file:///project/README.md',
|
||||
name: 'README.md',
|
||||
mimeType: 'text/markdown',
|
||||
description: 'A README file'
|
||||
},
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file:///project/src/index.ts',
|
||||
name: 'index.ts',
|
||||
mimeType: 'text/typescript',
|
||||
description: 'An index file'
|
||||
}
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### ResourceLinks
|
||||
|
||||
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.
|
||||
|
||||
### Resources
|
||||
|
||||
Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:
|
||||
[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.
|
||||
|
||||
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.
|
||||
|
||||
```typescript
|
||||
// Static resource
|
||||
|
|
@ -192,87 +327,9 @@ server.registerResource(
|
|||
);
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
|
||||
|
||||
```typescript
|
||||
// Simple tool with parameters
|
||||
server.registerTool(
|
||||
'calculate-bmi',
|
||||
{
|
||||
title: 'BMI Calculator',
|
||||
description: 'Calculate Body Mass Index',
|
||||
inputSchema: {
|
||||
weightKg: z.number(),
|
||||
heightM: z.number()
|
||||
}
|
||||
},
|
||||
async ({ weightKg, heightM }) => ({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: String(weightKg / (heightM * heightM))
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
// Async tool with external API call
|
||||
server.registerTool(
|
||||
'fetch-weather',
|
||||
{
|
||||
title: 'Weather Fetcher',
|
||||
description: 'Get weather data for a city',
|
||||
inputSchema: { city: z.string() }
|
||||
},
|
||||
async ({ city }) => {
|
||||
const response = await fetch(`https://api.weather.com/${city}`);
|
||||
const data = await response.text();
|
||||
return {
|
||||
content: [{ type: 'text', text: data }]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Tool that returns ResourceLinks
|
||||
server.registerTool(
|
||||
'list-files',
|
||||
{
|
||||
title: 'List Files',
|
||||
description: 'List project files',
|
||||
inputSchema: { pattern: z.string() }
|
||||
},
|
||||
async ({ pattern }) => ({
|
||||
content: [
|
||||
{ type: 'text', text: `Found files matching "${pattern}":` },
|
||||
// ResourceLinks let tools return references without file content
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file:///project/README.md',
|
||||
name: 'README.md',
|
||||
mimeType: 'text/markdown',
|
||||
description: 'A README file'
|
||||
},
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file:///project/src/index.ts',
|
||||
name: 'index.ts',
|
||||
mimeType: 'text/typescript',
|
||||
description: 'An index file'
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
#### ResourceLinks
|
||||
|
||||
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.
|
||||
|
||||
### Prompts
|
||||
|
||||
Prompts are reusable templates that help LLMs interact with your server effectively:
|
||||
[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.
|
||||
|
||||
```typescript
|
||||
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
||||
|
|
@ -364,7 +421,7 @@ const result = await client.complete({
|
|||
|
||||
### Display Names and Metadata
|
||||
|
||||
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.
|
||||
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`).
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -416,7 +473,8 @@ MCP servers can request LLM completions from connected clients that support samp
|
|||
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
const mcpServer = new McpServer({
|
||||
|
|
@ -428,10 +486,12 @@ const mcpServer = new McpServer({
|
|||
mcpServer.registerTool(
|
||||
'summarize',
|
||||
{
|
||||
title: 'Text Summarizer',
|
||||
description: 'Summarize any text using an LLM',
|
||||
inputSchema: {
|
||||
text: z.string().describe('Text to summarize')
|
||||
}
|
||||
},
|
||||
outputSchema: { summary: z.string() }
|
||||
},
|
||||
async ({ text }) => {
|
||||
// Call the LLM through MCP sampling
|
||||
|
|
@ -448,24 +508,36 @@ mcpServer.registerTool(
|
|||
maxTokens: 500
|
||||
});
|
||||
|
||||
const summary = response.content.type === 'text' ? response.content.text : 'Unable to generate summary';
|
||||
const output = { summary };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary'
|
||||
}
|
||||
]
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await mcpServer.connect(transport);
|
||||
console.error('MCP server is running...');
|
||||
}
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
main().catch(error => {
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
});
|
||||
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await mcpServer.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
app.listen(port, () => {
|
||||
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
||||
}).on('error', error => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -475,32 +547,92 @@ main().catch(error => {
|
|||
|
||||
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:
|
||||
|
||||
### stdio
|
||||
### Streamable HTTP
|
||||
|
||||
For command-line tools and direct integrations:
|
||||
For remote servers, use the Streamable HTTP transport.
|
||||
|
||||
#### Without Session Management (Recommended)
|
||||
|
||||
For most use cases where session management isn't needed:
|
||||
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Create the MCP server once (can be reused across requests)
|
||||
const server = new McpServer({
|
||||
name: 'example-server',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// ... set up server resources, tools, and prompts ...
|
||||
// Set up your tools, resources, and prompts
|
||||
server.registerTool(
|
||||
'echo',
|
||||
{
|
||||
title: 'Echo Tool',
|
||||
description: 'Echoes back the provided message',
|
||||
inputSchema: { message: z.string() },
|
||||
outputSchema: { echo: z.string() }
|
||||
},
|
||||
async ({ message }) => {
|
||||
const output = { echo: `Tool echo: ${message}` };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
app.post('/mcp', async (req, res) => {
|
||||
// In stateless mode, create a new transport for each request to prevent
|
||||
// request ID collisions. Different clients may use the same JSON-RPC request IDs,
|
||||
// which would cause responses to be routed to the wrong HTTP connections if
|
||||
// the transport state is shared.
|
||||
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
});
|
||||
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
app.listen(port, () => {
|
||||
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
||||
}).on('error', error => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Streamable HTTP
|
||||
|
||||
For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications.
|
||||
|
||||
#### With Session Management
|
||||
|
||||
In some cases, servers need to be stateful. This is achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management).
|
||||
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.
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
|
|
@ -591,8 +723,6 @@ app.delete('/mcp', handleSessionRequest);
|
|||
app.listen(3000);
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
#### CORS Configuration for Browser-Based Clients
|
||||
|
||||
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 +747,6 @@ This configuration is necessary because:
|
|||
- Browsers restrict access to response headers unless explicitly exposed via CORS
|
||||
- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses
|
||||
|
||||
#### Without Session Management (Stateless)
|
||||
|
||||
For simpler use cases where session management isn't needed:
|
||||
|
||||
```typescript
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
// In stateless mode, create a new instance of transport and server for each request
|
||||
// to ensure complete isolation. A single instance would cause request ID collisions
|
||||
// when multiple clients connect concurrently.
|
||||
|
||||
try {
|
||||
const server = getServer();
|
||||
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined
|
||||
});
|
||||
res.on('close', () => {
|
||||
console.log('Request closed');
|
||||
transport.close();
|
||||
server.close();
|
||||
});
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// SSE notifications not supported in stateless mode
|
||||
app.get('/mcp', async (req: Request, res: Response) => {
|
||||
console.log('Received GET MCP request');
|
||||
res.writeHead(405).end(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Method not allowed.'
|
||||
},
|
||||
id: null
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Session termination not needed in stateless mode
|
||||
app.delete('/mcp', async (req: Request, res: Response) => {
|
||||
console.log('Received DELETE MCP request');
|
||||
res.writeHead(405).end(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Method not allowed.'
|
||||
},
|
||||
id: null
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const PORT = 3000;
|
||||
setupServer()
|
||||
.then(() => {
|
||||
app.listen(PORT, error => {
|
||||
if (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to set up the server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
This stateless approach is useful for:
|
||||
|
||||
- Simple API wrappers
|
||||
- RESTful scenarios where each request is independent
|
||||
- Horizontally scaled deployments without shared session state
|
||||
|
||||
#### DNS Rebinding Protection
|
||||
|
||||
The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection is **disabled** for backwards compatibility.
|
||||
|
|
@ -727,6 +763,25 @@ const transport = new StreamableHTTPServerTransport({
|
|||
});
|
||||
```
|
||||
|
||||
### stdio
|
||||
|
||||
For local integrations spawned by another process, you can use the stdio transport:
|
||||
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'example-server',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// ... set up server resources, tools, and prompts ...
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
### Testing and Debugging
|
||||
|
||||
To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
|
||||
|
|
@ -746,6 +801,23 @@ const server = new McpServer({
|
|||
version: '1.0.0'
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
'echo',
|
||||
{
|
||||
title: 'Echo Tool',
|
||||
description: 'Echoes back the provided message',
|
||||
inputSchema: { message: z.string() },
|
||||
outputSchema: { echo: z.string() }
|
||||
},
|
||||
async ({ message }) => {
|
||||
const output = { echo: `Tool echo: ${message}` };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerResource(
|
||||
'echo',
|
||||
new ResourceTemplate('echo://{message}', { list: undefined }),
|
||||
|
|
@ -763,18 +835,6 @@ server.registerResource(
|
|||
})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'echo',
|
||||
{
|
||||
title: 'Echo Tool',
|
||||
description: 'Echoes back the provided message',
|
||||
inputSchema: { message: z.string() }
|
||||
},
|
||||
async ({ message }) => ({
|
||||
content: [{ type: 'text', text: `Tool echo: ${message}` }]
|
||||
})
|
||||
);
|
||||
|
||||
server.registerPrompt(
|
||||
'echo',
|
||||
{
|
||||
|
|
@ -851,19 +911,25 @@ server.registerTool(
|
|||
{
|
||||
title: 'SQL Query',
|
||||
description: 'Execute SQL queries on the database',
|
||||
inputSchema: { sql: z.string() }
|
||||
inputSchema: { sql: z.string() },
|
||||
outputSchema: {
|
||||
rows: z.array(z.record(z.any())),
|
||||
rowCount: z.number()
|
||||
}
|
||||
},
|
||||
async ({ sql }) => {
|
||||
const db = getDb();
|
||||
try {
|
||||
const results = await db.all(sql);
|
||||
const output = { rows: results, rowCount: results.length };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(results, null, 2)
|
||||
text: JSON.stringify(output, null, 2)
|
||||
}
|
||||
]
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const error = err as Error;
|
||||
|
|
@ -889,8 +955,10 @@ server.registerTool(
|
|||
|
||||
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:
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
const server = new McpServer({
|
||||
|
|
@ -898,23 +966,64 @@ const server = new McpServer({
|
|||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const listMessageTool = server.tool('listMessages', { channel: z.string() }, async ({ channel }) => ({
|
||||
content: [{ type: 'text', text: await listMessages(channel) }]
|
||||
}));
|
||||
const listMessageTool = server.registerTool(
|
||||
'listMessages',
|
||||
{
|
||||
title: 'List Messages',
|
||||
description: 'List messages in a channel',
|
||||
inputSchema: { channel: z.string() },
|
||||
outputSchema: { messages: z.array(z.string()) }
|
||||
},
|
||||
async ({ channel }) => {
|
||||
const messages = await listMessages(channel);
|
||||
const output = { messages };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const putMessageTool = server.tool('putMessage', { channel: z.string(), message: z.string() }, async ({ channel, message }) => ({
|
||||
content: [{ type: 'text', text: await putMessage(channel, message) }]
|
||||
}));
|
||||
const putMessageTool = server.registerTool(
|
||||
'putMessage',
|
||||
{
|
||||
title: 'Put Message',
|
||||
description: 'Send a message to a channel',
|
||||
inputSchema: { channel: z.string(), message: z.string() },
|
||||
outputSchema: { success: z.boolean() }
|
||||
},
|
||||
async ({ channel, message }) => {
|
||||
await putMessage(channel, message);
|
||||
const output = { success: true };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
// Until we upgrade auth, `putMessage` is disabled (won't show up in listTools)
|
||||
putMessageTool.disable();
|
||||
|
||||
const upgradeAuthTool = server.tool(
|
||||
const upgradeAuthTool = server.registerTool(
|
||||
'upgradeAuth',
|
||||
{ permission: z.enum(['write', 'admin']) },
|
||||
{
|
||||
title: 'Upgrade Authorization',
|
||||
description: 'Upgrade user authorization level',
|
||||
inputSchema: { permission: z.enum(['write', 'admin']) },
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
newPermission: z.string()
|
||||
}
|
||||
},
|
||||
// Any mutations here will automatically emit `listChanged` notifications
|
||||
async ({ permission }) => {
|
||||
const { ok, err, previous } = await upgradeAuthAndStoreToken(permission);
|
||||
if (!ok) return { content: [{ type: 'text', text: `Error: ${err}` }] };
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${err}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// If we previously had read-only access, 'putMessage' is now available
|
||||
if (previous === 'read') {
|
||||
|
|
@ -931,12 +1040,37 @@ const upgradeAuthTool = server.tool(
|
|||
// If we're now an admin, we no longer have anywhere to upgrade to, so fully remove that tool
|
||||
upgradeAuthTool.remove();
|
||||
}
|
||||
|
||||
const output = { success: true, newPermission: permission };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Connect as normal
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// Connect with HTTP transport
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
});
|
||||
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
app.listen(port, () => {
|
||||
console.log(`MCP Server running on http://localhost:${port}/mcp`);
|
||||
});
|
||||
```
|
||||
|
||||
### Improving Network Efficiency with Notification Debouncing
|
||||
|
|
@ -1043,12 +1177,27 @@ MCP servers can request additional information from users through the elicitatio
|
|||
|
||||
```typescript
|
||||
// Server-side: Restaurant booking tool that asks for alternatives
|
||||
server.tool(
|
||||
server.registerTool(
|
||||
'book-restaurant',
|
||||
{
|
||||
restaurant: z.string(),
|
||||
date: z.string(),
|
||||
partySize: z.number()
|
||||
title: 'Book Restaurant',
|
||||
description: 'Book a table at a restaurant',
|
||||
inputSchema: {
|
||||
restaurant: z.string(),
|
||||
date: z.string(),
|
||||
partySize: z.number()
|
||||
},
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
booking: z
|
||||
.object({
|
||||
restaurant: z.string(),
|
||||
date: z.string(),
|
||||
partySize: z.number()
|
||||
})
|
||||
.optional(),
|
||||
alternatives: z.array(z.string()).optional()
|
||||
}
|
||||
},
|
||||
async ({ restaurant, date, partySize }) => {
|
||||
// Check availability
|
||||
|
|
@ -1080,35 +1229,44 @@ server.tool(
|
|||
|
||||
if (result.action === 'accept' && result.content?.checkAlternatives) {
|
||||
const alternatives = await findAlternatives(restaurant, date, partySize, result.content.flexibleDates as string);
|
||||
const output = { success: false, alternatives };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found these alternatives: ${alternatives.join(', ')}`
|
||||
text: JSON.stringify(output)
|
||||
}
|
||||
]
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
|
||||
const output = { success: false };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No booking made. Original date not available.'
|
||||
text: JSON.stringify(output)
|
||||
}
|
||||
]
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
|
||||
// Book the table
|
||||
await makeBooking(restaurant, date, partySize);
|
||||
const output = {
|
||||
success: true,
|
||||
booking: { restaurant, date, partySize }
|
||||
};
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
|
||||
text: JSON.stringify(output)
|
||||
}
|
||||
]
|
||||
],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue