diff --git a/.changeset/config.json b/.changeset/config.json index bae546e8e..eb43bdc7f 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,11 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@modelcontextprotocol/examples-client", "@modelcontextprotocol/examples-server", "@modelcontextprotocol/examples-shared"] + "ignore": [ + "@modelcontextprotocol/examples-client", + "@modelcontextprotocol/examples-client-quickstart", + "@modelcontextprotocol/examples-server", + "@modelcontextprotocol/examples-server-quickstart", + "@modelcontextprotocol/examples-shared" + ] } diff --git a/.prettierignore b/.prettierignore index ae37f91c7..1aecab1f5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,7 @@ pnpm-lock.yaml # Ignore generated files src/spec.types.ts + +# Quickstart examples uses 2-space indent to match ecosystem conventions +examples/client-quickstart/ +examples/server-quickstart/ diff --git a/README.md b/README.md index 745887313..3bd832f88 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This repository contains the TypeScript SDK implementation of the MCP specificat - MCP **server** libraries (tools/resources/prompts, Streamable HTTP, stdio, auth helpers) - MCP **client** libraries (transports, high-level helpers, OAuth helpers) - Optional **middleware packages** for specific runtimes/frameworks (Express, Hono, Node.js HTTP) -- Runnable **examples** (under [`examples/`](examples/)) +- Runnable **examples** (under [`examples/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)) ## Packages @@ -43,7 +43,7 @@ Both packages have a **required peer dependency** on `zod` for schema validation ### Middleware packages (optional) -The SDK also publishes small “middleware” packages under [`packages/middleware/`](packages/middleware/) that help you **wire MCP into a specific runtime or web framework**. +The SDK also publishes small "middleware" packages under [`packages/middleware/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/middleware) that help you **wire MCP into a specific runtime or web framework**. They are intentionally thin adapters: they should not introduce new MCP functionality or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details. @@ -127,9 +127,8 @@ Next steps: ## Documentation - Local SDK docs: - - [docs/server.md](docs/server.md) – building MCP servers, transports, tools/resources/prompts, CORS, DNS rebinding, and deployment patterns. - - [docs/client.md](docs/client.md) – using the high-level client, transports, backwards compatibility, and OAuth helpers. - - [docs/capabilities.md](docs/capabilities.md) – sampling, elicitation (form and URL), and experimental task-based execution. + - [docs/server.md](docs/server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns. + - [docs/client.md](docs/client.md) – using the high-level client, transports, OAuth helpers, handling server‑initiated requests, and tasks. - [docs/faq.md](docs/faq.md) – environment and troubleshooting FAQs (including Node.js Web Crypto support). - External references: - [SDK API documentation](https://modelcontextprotocol.github.io/typescript-sdk/) diff --git a/docs/capabilities.md b/docs/capabilities.md deleted file mode 100644 index 579a0f09c..000000000 --- a/docs/capabilities.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Capabilities ---- - -## Sampling - -MCP servers can request LLM completions from connected clients that support the sampling capability. This lets your tools offload summarisation or generation to the client’s model. - -For a runnable server that combines tools, logging and tasks, see: - -- [`toolWithSampleServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/toolWithSampleServer.ts) - -In practice you will: - -- Declare the sampling capability on the client. -- Call `server.server.createMessage(...)` from within a tool handler. -- Return the model’s response as structured content and/or text. - -Refer to the MCP spec’s sampling section for full request/response details. - -## Elicitation - -### Form elicitation - -Form elicitation lets a tool ask the user for additional, **non‑sensitive** information via a schema‑driven form. The server sends a schema and message, and the client is responsible for collecting and returning the data. - -Runnable example: - -- Server: [`elicitationFormExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationFormExample.ts) -- Client‑side handling: [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts) - -The `simpleStreamableHttp` server also includes a `collect-user-info` tool that demonstrates how to drive elicitation from a tool and handle the response. - -### URL elicitation - -URL elicitation is designed for sensitive data and secure web‑based flows (e.g., collecting an API key, confirming a payment, or doing third‑party OAuth). Instead of returning form data, the server asks the client to open a URL and the rest of the flow happens in the browser. - -Runnable example: - -- Server: [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationUrlExample.ts) -- Client: [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts) - -Key points: - -- Use `mode: 'url'` when calling `server.server.elicitInput(...)`. -- Implement a client‑side handler for `ElicitRequestSchema` that: - - Shows the full URL and reason to the user. - - Asks for explicit consent. - - Opens the URL in the system browser. - -Sensitive information **must not** be collected via form elicitation; always use URL elicitation or out‑of‑band flows for secrets. - -## Task-based execution (experimental) - -Task-based execution enables “call-now, fetch-later” patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. - -The APIs live under the experimental `.experimental.tasks` namespace and may change without notice. - -### Server-side concepts - -On the server you will: - -- Provide a `TaskStore` implementation that persists task metadata and results. -- Enable the `tasks` capability when constructing the server. -- Register tools with `server.experimental.tasks.registerToolTask(...)`. - -For a runnable example that uses the in-memory store shipped with the SDK, see: - -- [`toolWithSampleServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/toolWithSampleServer.ts) -- `packages/core/src/experimental/tasks/stores/in-memory.ts` - -### Client-side usage - -On the client, you use: - -- `client.experimental.tasks.callToolStream(...)` to start a tool call that may create a task and emit status updates over time. -- `client.getTask(...)` and `client.getTaskResult(...)` to check status and fetch results after reconnecting. - -The interactive client in: - -- [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts) - -includes commands to demonstrate calling tools that support tasks and handling their lifecycle. - -See the MCP spec’s tasks section and the example server/client above for a full walkthrough of the task status lifecycle and TTL handling. diff --git a/docs/client-quickstart.md b/docs/client-quickstart.md new file mode 100644 index 000000000..bd695c7bb --- /dev/null +++ b/docs/client-quickstart.md @@ -0,0 +1,420 @@ +--- +title: Client Quickstart +--- + +# Quickstart: Build an LLM-powered chatbot + +In this tutorial, we'll build an LLM-powered chatbot that connects to an MCP server, discovers its tools, and uses Claude to call them. + +Before you begin, it helps to have gone through the [server quickstart](./server-quickstart.md) so you understand how clients and servers communicate. + +[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client-quickstart) + +## Prerequisites + +This quickstart assumes you have familiarity with: + +- TypeScript +- LLMs like Claude + +Before starting, ensure your system meets these requirements: + +- Node.js 20 or higher installed +- Latest version of `npm` installed +- An Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys) + +## Set up your environment + +First, let's create and set up our project: + +**macOS/Linux:** + +```bash +# Create project directory +mkdir mcp-client +cd mcp-client + +# Initialize npm project +npm init -y + +# Install dependencies +npm install @anthropic-ai/sdk @modelcontextprotocol/client + +# Install dev dependencies +npm install -D @types/node typescript + +# Create source file +mkdir src +touch src/index.ts +``` + +**Windows:** + +```powershell +# Create project directory +md mcp-client +cd mcp-client + +# Initialize npm project +npm init -y + +# Install dependencies +npm install @anthropic-ai/sdk @modelcontextprotocol/client + +# Install dev dependencies +npm install -D @types/node typescript + +# Create source file +md src +new-item src\index.ts +``` + +Update your `package.json` to set `type: "module"` and a build script: + +```json +{ + "type": "module", + "scripts": { + "build": "tsc" + } +} +``` + +Create a `tsconfig.json` in the root of your project: + +```json +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +## Creating the client + +### Basic client structure + +First, let's set up our imports and create the basic client class in `src/index.ts`: + +```ts source="../examples/client-quickstart/src/index.ts#prelude" +import Anthropic from '@anthropic-ai/sdk'; +import { Client, StdioClientTransport, type CallToolResult } from '@modelcontextprotocol/client'; +import readline from 'readline/promises'; + +const ANTHROPIC_MODEL = 'claude-sonnet-4-5'; + +class MCPClient { + private mcp: Client; + private _anthropic: Anthropic | null = null; + private transport: StdioClientTransport | null = null; + private tools: Anthropic.Tool[] = []; + + constructor() { + // Initialize MCP client + this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' }); + } + + private get anthropic(): Anthropic { + // Lazy-initialize Anthropic client when needed + return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + } +``` + +### Server connection management + +Next, we'll implement the method to connect to an MCP server: + +```ts source="../examples/client-quickstart/src/index.ts#connectToServer" + async connectToServer(serverScriptPath: string) { + try { + // Determine script type and appropriate command + const isJs = serverScriptPath.endsWith('.js'); + const isPy = serverScriptPath.endsWith('.py'); + if (!isJs && !isPy) { + throw new Error('Server script must be a .js or .py file'); + } + const command = isPy + ? (process.platform === 'win32' ? 'python' : 'python3') + : process.execPath; + + // Initialize transport and connect to server + this.transport = new StdioClientTransport({ command, args: [serverScriptPath] }); + await this.mcp.connect(this.transport); + + // List available tools + const toolsResult = await this.mcp.listTools(); + this.tools = toolsResult.tools.map((tool) => ({ + name: tool.name, + description: tool.description ?? '', + input_schema: tool.inputSchema as Anthropic.Tool.InputSchema, + })); + console.log('Connected to server with tools:', this.tools.map(({ name }) => name)); + } catch (e) { + console.log('Failed to connect to MCP server: ', e); + throw e; + } + } +``` + +### Query processing logic + +Now let's add the core functionality for processing queries and handling tool calls: + +```ts source="../examples/client-quickstart/src/index.ts#processQuery" + async processQuery(query: string) { + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: query, + }, + ]; + + // Initial Claude API call + const response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); + + // Process response and handle tool calls + const finalText = []; + + for (const content of response.content) { + if (content.type === 'text') { + finalText.push(content.text); + } else if (content.type === 'tool_use') { + // Execute tool call + const toolName = content.name; + const toolArgs = content.input as Record | undefined; + const result = await this.mcp.callTool({ + name: toolName, + arguments: toolArgs, + }) as CallToolResult; + + finalText.push(`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`); + + // Extract text from tool result content blocks + const toolResultText = result.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + + // Continue conversation with tool results + messages.push({ + role: 'assistant', + content: response.content, + }); + messages.push({ + role: 'user', + content: [{ + type: 'tool_result', + tool_use_id: content.id, + content: toolResultText, + }], + }); + + // Get next response from Claude + const followUp = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + }); + + finalText.push(followUp.content[0].type === 'text' ? followUp.content[0].text : ''); + } + } + + return finalText.join('\n'); + } +``` + +### Interactive chat interface + +Now we'll add the chat loop and cleanup functionality: + +```ts source="../examples/client-quickstart/src/index.ts#chatLoop" + async chatLoop() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + console.log('\nMCP Client Started!'); + console.log('Type your queries or "quit" to exit.'); + + while (true) { + const message = await rl.question('\nQuery: '); + if (message.toLowerCase() === 'quit') { + break; + } + const response = await this.processQuery(message); + console.log('\n' + response); + } + } finally { + rl.close(); + } + } + + async cleanup() { + await this.mcp.close(); + } +} +``` + +### Main entry point + +Finally, we'll add the main execution logic: + +```ts source="../examples/client-quickstart/src/index.ts#main" +async function main() { + if (process.argv.length < 3) { + console.log('Usage: node build/index.js '); + return; + } + const mcpClient = new MCPClient(); + try { + await mcpClient.connectToServer(process.argv[2]); + + // Check if we have a valid API key to continue + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + console.log( + '\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:' + + '\n export ANTHROPIC_API_KEY=your-api-key-here' + ); + return; + } + + await mcpClient.chatLoop(); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await mcpClient.cleanup(); + process.exit(0); + } +} + +main(); +``` + +## Running the client + +To run your client with any MCP server: + +**macOS/Linux:** + +```bash +# Build TypeScript +npm run build + +# Run the client with a Node.js MCP server +ANTHROPIC_API_KEY=your-key-here node build/index.js path/to/server/build/index.js + +# Example: connect to the weather server from the server quickstart +ANTHROPIC_API_KEY=your-key-here node build/index.js /absolute/path/to/weather/build/index.js +``` + +**Windows:** + +```powershell +# Build TypeScript +npm run build + +# Run the client with a Node.js MCP server +$env:ANTHROPIC_API_KEY="your-key-here"; node build/index.js path\to\server\build\index.js +``` + +**The client will:** + +1. Connect to the specified server +2. List available tools +3. Start an interactive chat session where you can: + - Enter queries + - See tool executions + - Get responses from Claude + +## What's happening under the hood + +When you submit a query: + +1. Your query is sent to Claude along with the tool descriptions discovered during connection +2. Claude decides which tools (if any) to use +3. The client executes any requested tool calls through the server +4. Results are sent back to Claude +5. Claude provides a natural language response +6. The response is displayed to you + +## Troubleshooting + +### Server Path Issues + +- Double-check the path to your server script is correct +- Use the absolute path if the relative path isn't working +- For Windows users, make sure to use forward slashes (`/`) or escaped backslashes (`\\`) in the path +- Verify the server file has the correct extension (`.js` for Node.js or `.py` for Python) + +Example of correct path usage: + +**macOS/Linux:** + +```bash +# Relative path +node build/index.js ./server/build/index.js + +# Absolute path +node build/index.js /Users/username/projects/mcp-server/build/index.js +``` + +**Windows:** + +```powershell +# Relative path +node build/index.js .\server\build\index.js + +# Absolute path (either format works) +node build/index.js C:\projects\mcp-server\build\index.js +node build/index.js C:/projects/mcp-server/build/index.js +``` + +### Response Timing + +- The first response might take up to 30 seconds to return +- This is normal and happens while: + - The server initializes + - Claude processes the query + - Tools are being executed +- Subsequent responses are typically faster +- Don't interrupt the process during this initial waiting period + +### Common Error Messages + +If you see: + +- `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded +- `Connection refused`: Ensure the server is running and the path is correct +- `Tool execution failed`: Verify the tool's required environment variables are set +- `ANTHROPIC_API_KEY is not set`: Check your environment variables (e.g., `export ANTHROPIC_API_KEY=...`) +- `TypeError`: Ensure you're using the correct types for tool arguments +- `BadRequestError`: Ensure you have enough credits to access the Anthropic API + +## Next steps + +Now that you have a working client, here are some ways to go further: + +- [**Client guide**](./client.md) — Add OAuth, middleware, sampling, and more to your client. +- [**Example clients**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Browse runnable client examples. +- [**FAQ**](./faq.md) — Troubleshoot common errors. diff --git a/docs/client.md b/docs/client.md index ae49d759d..3a6e9973b 100644 --- a/docs/client.md +++ b/docs/client.md @@ -1,8 +1,8 @@ --- -title: Client +title: Client Guide --- -## Client overview +# Client overview This guide covers SDK usage for building MCP clients in TypeScript. For protocol-level details and message formats, see the [MCP specification](https://modelcontextprotocol.io/specification/latest/). @@ -318,7 +318,7 @@ client.setRequestHandler('elicitation/create', async request => { ``` > [!NOTE] -> For a full form‑based elicitation handler with AJV validation, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). For URL elicitation mode, see [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts) and the [Capabilities guide](capabilities.md#elicitation). +> For a full form‑based elicitation handler with AJV validation, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). For URL elicitation mode (`mode: 'url'`), see [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts). > > For protocol details, see [Elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation) in the MCP specification. @@ -367,6 +367,19 @@ console.log(result); > [!NOTE] > For an end‑to‑end example of server‑initiated SSE disconnection and automatic client reconnection with event replay, see [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts). +## Tasks (experimental) + +Task-based execution enables "call-now, fetch-later" patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks: + +- Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#callToolStream | client.experimental.tasks.callToolStream(...)} to start a tool call that may create a task and emit status updates over time. +- Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTask | client.experimental.tasks.getTask(...)} and {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTaskResult | getTaskResult(...)} to check status and fetch results after reconnecting. + +> [!NOTE] +> For a full runnable example, see [`simpleTaskInteractiveClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTaskInteractiveClient.ts). + +> [!WARNING] +> The tasks API is experimental and may change without notice. + ## More client features The sections above cover the essentials. The table below links to additional capabilities. @@ -377,4 +390,3 @@ The sections above cover the essentials. The table below links to additional cap | SSE disconnect / reconnection | Server‑initiated SSE disconnect with automatic reconnection and event replay | [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts) | | Multiple clients | Independent client lifecycles to the same server | [`multipleClientsParallel.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/multipleClientsParallel.ts) | | URL elicitation | Handle sensitive data collection via browser | [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts) | -| Tasks (experimental) | Long‑running tool calls with status streaming | [`simpleTaskInteractiveClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTaskInteractiveClient.ts), [Capabilities guide](capabilities.md#task-based-execution-experimental) | diff --git a/docs/documents.md b/docs/documents.md index 36b8235cb..37033ef0c 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -1,15 +1,17 @@ --- title: Documents children: + - ./server-quickstart.md - ./server.md + - ./client-quickstart.md - ./client.md - - ./capabilities.md - ./faq.md --- # Documents -- [Server](./server.md) – building MCP servers, transports, tools/resources/prompts, and deployment patterns -- [Client](./client.md) – using the high-level client, transports, backwards compatibility, and OAuth helpers -- [Capabilities](./capabilities.md) – sampling, elicitation, and experimental task-based execution +- [Server Quickstart](./server-quickstart.md) – build a weather server from scratch and connect it to Claude for Desktop +- [Server](./server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns +- [Client Quickstart](./client-quickstart.md) – build an LLM-powered chatbot that connects to an MCP server and calls its tools +- [Client](./client.md) – using the high-level client, transports, OAuth helpers, handling server‑initiated requests, and tasks - [FAQ](./faq.md) – frequently asked questions and troubleshooting diff --git a/docs/server-quickstart.md b/docs/server-quickstart.md new file mode 100644 index 000000000..d0a7e6ed0 --- /dev/null +++ b/docs/server-quickstart.md @@ -0,0 +1,536 @@ +--- +title: Server Quickstart +--- + +# Quickstart: Build a weather server + +In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop. + +## What we'll be building + +We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude for Desktop). + +> [!NOTE] +> Servers can connect to any client. We've chosen Claude for Desktop here for simplicity, but we also have a guide on [building your own client](./client.md) as well as a [list of other clients here](https://modelcontextprotocol.io/clients). + +## Core MCP Concepts + +MCP servers can provide three main types of capabilities: + +1. **[Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources)**: File-like data that can be read by clients (like API responses or file contents) +2. **[Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools)**: Functions that can be called by the LLM (with user approval) +3. **[Prompts](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts)**: Pre-written templates that help users accomplish specific tasks + +This tutorial will primarily focus on tools. + +Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server-quickstart) + +## Prerequisites + +This quickstart assumes you have familiarity with: + +- TypeScript +- LLMs like Claude + +Make sure you have Node.js version 20 or higher installed. You can verify your installation: + +```bash +node --version +npm --version +``` + +## Set up your environment + +First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/). + +Now, let's create and set up our project: + +**macOS/Linux:** + +```bash +# Create a new directory for our project +mkdir weather +cd weather + +# Initialize a new npm project +npm init -y + +# Install dependencies +npm install @modelcontextprotocol/server zod +npm install -D @types/node typescript + +# Create our files +mkdir src +touch src/index.ts +``` + +**Windows:** + +```powershell +# Create a new directory for our project +md weather +cd weather + +# Initialize a new npm project +npm init -y + +# Install dependencies +npm install @modelcontextprotocol/server zod +npm install -D @types/node typescript + +# Create our files +md src +new-item src\index.ts +``` + +Update your `package.json` to add `type: "module"` and a build script: + +```json +{ + "type": "module", + "bin": { + "weather": "./build/index.js" + }, + "scripts": { + "build": "tsc && chmod 755 build/index.js" + }, + "files": ["build"] +} +``` + +Create a `tsconfig.json` in the root of your project: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +Now let's dive into building your server. + +## Building your server + +### Importing packages and setting up the instance + +Add these to the top of your `src/index.ts`: + +```ts source="../examples/server-quickstart/src/index.ts#prelude" +import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server'; +import * as z from 'zod/v4'; + +const NWS_API_BASE = 'https://api.weather.gov'; +const USER_AGENT = 'weather-app/1.0'; + +// Create server instance +const server = new McpServer({ + name: 'weather', + version: '1.0.0', +}); +``` + +### Helper functions + +Next, let's add our helper functions for querying and formatting the data from the National Weather Service API: + +```ts source="../examples/server-quickstart/src/index.ts#helpers" +// Helper function for making NWS API requests +async function makeNWSRequest(url: string): Promise { + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/geo+json', + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error('Error making NWS request:', error); + return null; + } +} + +interface AlertFeature { + properties: { + event?: string; + areaDesc?: string; + severity?: string; + status?: string; + headline?: string; + }; +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || 'Unknown'}`, + `Area: ${props.areaDesc || 'Unknown'}`, + `Severity: ${props.severity || 'Unknown'}`, + `Status: ${props.status || 'Unknown'}`, + `Headline: ${props.headline || 'No headline'}`, + '---', + ].join('\n'); +} + +interface ForecastPeriod { + name?: string; + temperature?: number; + temperatureUnit?: string; + windSpeed?: string; + windDirection?: string; + shortForecast?: string; +} + +interface AlertsResponse { + features: AlertFeature[]; +} + +interface PointsResponse { + properties: { + forecast?: string; + }; +} + +interface ForecastResponse { + properties: { + periods: ForecastPeriod[]; + }; +} +``` + +### Registering tools + +Each tool is registered with {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | server.registerTool()}, which takes the tool name, a configuration object (with description and input schema), and a callback that implements the tool logic. Let's register our two weather tools: + +```ts source="../examples/server-quickstart/src/index.ts#registerTools" +// Register weather tools +server.registerTool( + 'get-alerts', + { + title: 'Get Weather Alerts', + description: 'Get weather alerts for a state', + inputSchema: z.object({ + state: z.string().length(2) + .describe('Two-letter state code (e.g. CA, NY)'), + }), + }, + async ({ state }) => { + const stateCode = state.toUpperCase(); + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest(alertsUrl); + + if (!alertsData) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to retrieve alerts data', + }], + }; + } + + const features = alertsData.features || []; + + if (features.length === 0) { + return { + content: [{ + type: 'text' as const, + text: `No active alerts for ${stateCode}`, + }], + }; + } + + const formattedAlerts = features.map(formatAlert); + + return { + content: [{ + type: 'text' as const, + text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`, + }], + }; + }, +); + +server.registerTool( + 'get-forecast', + { + title: 'Get Weather Forecast', + description: 'Get weather forecast for a location', + inputSchema: z.object({ + latitude: z.number().min(-90).max(90) + .describe('Latitude of the location'), + longitude: z.number().min(-180).max(180) + .describe('Longitude of the location'), + }), + }, + async ({ latitude, longitude }) => { + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest(pointsUrl); + + if (!pointsData) { + return { + content: [{ + type: 'text' as const, + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to get forecast URL from grid point data', + }], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest(forecastUrl); + if (!forecastData) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to retrieve forecast data', + }], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [{ + type: 'text' as const, + text: 'No forecast periods available', + }], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || 'Unknown'}:`, + `Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`, + `Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`, + `${period.shortForecast || 'No forecast available'}`, + '---', + ].join('\n'), + ); + + return { + content: [{ + type: 'text' as const, + text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`, + }], + }; + }, +); +``` + +### Running the server + +Finally, implement the main function to run the server: + +```ts source="../examples/server-quickstart/src/index.ts#main" +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Weather MCP Server running on stdio'); +} + +main().catch((error) => { + console.error('Fatal error in main():', error); + process.exit(1); +}); +``` + +> [!IMPORTANT] +> Always use `console.error()` instead of `console.log()` in stdio-based MCP servers. Standard output is reserved for JSON-RPC protocol messages, and writing to it with `console.log()` will corrupt the communication channel. + +Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect. + +Let's now test your server from an existing MCP host, Claude for Desktop. + +## Testing your server with Claude for Desktop + +> [!NOTE] +> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Client guide](./client.md) to build an MCP client that connects to the server we just built. + +First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.** + +We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist. + +For example, if you have [VS Code](https://code.visualstudio.com/) installed: + +**macOS/Linux:** + +```bash +code ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +**Windows:** + +```powershell +code $env:AppData\Claude\claude_desktop_config.json +``` + +You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured. + +In this case, we'll add our single weather server like so: + +**macOS/Linux:** + +```json +{ + "mcpServers": { + "weather": { + "command": "node", + "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"] + } + } +} +``` + +**Windows:** + +```json +{ + "mcpServers": { + "weather": { + "command": "node", + "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"] + } + } +} +``` + +This tells Claude for Desktop: + +1. There's an MCP server named "weather" +2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js` + +Save the file, and restart **Claude for Desktop**. + +### Test with commands + +Let's make sure Claude for Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the "Add files, connectors, and more /" icon. + +After clicking on the plus icon, hover over the "Connectors" menu. You should see the `weather` server listed. + +If your server isn't being picked up by Claude for Desktop, proceed to the [Troubleshooting](#troubleshooting) section for debugging tips. + +If the server has shown up in the "Connectors" menu, you can now test your server by running the following commands in Claude for Desktop: + +- What's the weather in Sacramento? +- What are the active weather alerts in Texas? + +> [!NOTE] +> Since this is the US National Weather Service, the queries will only work for US locations. + +## What's happening under the hood + +When you ask a question: + +1. The client sends your question to Claude +2. Claude analyzes the available tools and decides which one(s) to use +3. The client executes the chosen tool(s) through the MCP server +4. The results are sent back to Claude +5. Claude formulates a natural language response +6. The response is displayed to you! + +## Troubleshooting + +
+Claude for Desktop integration issues + +**Getting logs from Claude for Desktop** + +Claude.app logging related to MCP is written to log files in `~/Library/Logs/Claude`: + +- `mcp.log` will contain general logging about MCP connections and connection failures. +- Files named `mcp-server-SERVERNAME.log` will contain error (stderr) logging from the named server. + +You can run the following command to list recent logs and follow along with any new ones: + +```bash +# Check Claude's logs for errors +tail -n 20 -f ~/Library/Logs/Claude/mcp*.log +``` + +**Server not showing up in Claude** + +1. Check your `claude_desktop_config.json` file syntax +2. Make sure the path to your project is absolute and not relative +3. Restart Claude for Desktop completely + +> [!WARNING] +> To properly restart Claude for Desktop, you must fully quit the application: +> +> - **Windows**: Right-click the Claude icon in the system tray (which may be hidden in the "hidden icons" menu) and select "Quit" or "Exit". +> - **macOS**: Use Cmd+Q or select "Quit Claude" from the menu bar. +> +> Simply closing the window does not fully quit the application, and your MCP server configuration changes will not take effect. + +**Tool calls failing silently** + +If Claude attempts to use the tools but they fail: + +1. Check Claude's logs for errors +2. Verify your server builds and runs without errors +3. Try restarting Claude for Desktop + +**None of this is working. What do I do?** + +Please refer to our [debugging guide](https://modelcontextprotocol.io/legacy/tools/debugging) for better debugging tools and more detailed guidance. + +
+ +
+Weather API issues + +**Error: Failed to retrieve grid point data** + +This usually means either: + +1. The coordinates are outside the US +2. The NWS API is having issues +3. You're being rate limited + +Fix: + +- Verify you're using US coordinates +- Add a small delay between requests +- Check the NWS API status page + +**Error: No active alerts for [STATE]** + +This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather. + +
+ +> [!NOTE] +> For more advanced troubleshooting, check out our guide on [Debugging MCP](https://modelcontextprotocol.io/legacy/tools/debugging). + +## Next steps + +Now that your server is running locally, here are some ways to go further: + +- [**Server guide**](./server.md) — Add resources, prompts, logging, error handling, and remote transports to your server. +- [**Example servers**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server) — Browse runnable examples covering OAuth, streaming, sessions, and more. +- [**FAQ**](./faq.md) — Troubleshoot common errors (Zod version conflicts, transport issues, etc.). diff --git a/docs/server.md b/docs/server.md index 941ca8740..7a6a5d9fe 100644 --- a/docs/server.md +++ b/docs/server.md @@ -1,8 +1,8 @@ --- -title: Server +title: Server Guide --- -## Server overview +# Server overview This guide covers SDK usage for building MCP servers in TypeScript. For protocol-level details and message formats, see the [MCP specification](https://modelcontextprotocol.io/specification/latest/). @@ -92,30 +92,6 @@ const transport = new StdioServerTransport(); await server.connect(transport); ``` -## DNS rebinding protection - -MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` from `@modelcontextprotocol/express` to create an Express app with DNS rebinding protection enabled by default: - -```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_basic" -// Default: DNS rebinding protection auto-enabled (host is 127.0.0.1) -const app = createMcpExpressApp(); - -// DNS rebinding protection also auto-enabled for localhost -const appLocal = createMcpExpressApp({ host: 'localhost' }); - -// No automatic protection when binding to all interfaces -const appOpen = createMcpExpressApp({ host: '0.0.0.0' }); -``` - -When binding to `0.0.0.0` / `::`, provide an allow-list of hosts: - -```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_allowedHosts" -const app = createMcpExpressApp({ - host: '0.0.0.0', - allowedHosts: ['localhost', '127.0.0.1', 'myhost.local'] -}); -``` - ## Tools, resources, and prompts ### Tools @@ -185,37 +161,12 @@ server.registerTool( > [!NOTE] > For a full runnable example with `ResourceLink` outputs, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts). -#### Logging +#### Tool annotations -Use `ctx.mcpReq.log(level, data)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler to send structured log messages to the client. The server must declare the `logging` capability: - -```ts source="../examples/server/src/serverGuide.examples.ts#logging_capability" -const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } }); -``` - -Then log from any tool callback: - -```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_logging" -server.registerTool( - 'fetch-data', - { - description: 'Fetch data from an API', - inputSchema: z.object({ url: z.string() }) - }, - async ({ url }, ctx): Promise => { - await ctx.mcpReq.log('info', `Fetching ${url}`); - const res = await fetch(url); - await ctx.mcpReq.log('debug', `Response status: ${res.status}`); - const text = await res.text(); - return { content: [{ type: 'text', text }] }; - } -); -``` +Tools can include annotations that hint at their behavior — for example, whether a tool is read‑only, destructive, or idempotent. Annotations help clients present tools appropriately without changing their execution semantics. > [!NOTE] -> For logging in a full server, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) and [`jsonResponseStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/jsonResponseStreamableHttp.ts). -> -> For protocol details, see [Logging](https://modelcontextprotocol.io/specification/latest/server/utilities/logging) in the MCP specification. +> For tool annotations in a full server, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts). ### Resources @@ -337,11 +288,181 @@ server.registerPrompt( ); ``` -For client-side completion usage, see the [Client guide](client.md). +### Logging + +Unlike tools, resources, and prompts, logging is not a registered primitive — it is a handler-level API available inside any callback. Use `ctx.mcpReq.log(level, data)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) to send structured log messages to the client. The server must declare the `logging` capability: + +```ts source="../examples/server/src/serverGuide.examples.ts#logging_capability" +const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } }); +``` + +Then log from any handler callback: + +```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_logging" +server.registerTool( + 'fetch-data', + { + description: 'Fetch data from an API', + inputSchema: z.object({ url: z.string() }) + }, + async ({ url }, ctx): Promise => { + await ctx.mcpReq.log('info', `Fetching ${url}`); + const res = await fetch(url); + await ctx.mcpReq.log('debug', `Response status: ${res.status}`); + const text = await res.text(); + return { content: [{ type: 'text', text }] }; + } +); +``` + +> [!NOTE] +> For logging in a full server, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) and [`jsonResponseStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/jsonResponseStreamableHttp.ts). +> +> For protocol details, see [Logging](https://modelcontextprotocol.io/specification/latest/server/utilities/logging) in the MCP specification. + +## Server‑initiated requests + +MCP is bidirectional — servers can also send requests *to* the client during tool execution, as long as the client declares matching capabilities. + +### Sampling + +Use `ctx.mcpReq.requestSampling(params)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler to request an LLM completion from the connected client: + +```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_sampling" +server.registerTool( + 'summarize', + { + description: 'Summarize text using the client LLM', + inputSchema: z.object({ text: z.string() }) + }, + async ({ text }, ctx): Promise => { + const response = await ctx.mcpReq.requestSampling({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please summarize:\n\n${text}` + } + } + ], + maxTokens: 500 + }); + return { + content: [ + { + type: 'text', + text: `Model (${response.model}): ${JSON.stringify(response.content)}` + } + ] + }; + } +); +``` + +> [!NOTE] +> For a full runnable example, see [`toolWithSampleServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/toolWithSampleServer.ts). +> +> For protocol details, see [Sampling](https://modelcontextprotocol.io/specification/latest/client/sampling) in the MCP specification. + +### Elicitation + +Use `ctx.mcpReq.elicitInput(params)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler to request user input. Elicitation supports two modes: + +- **Form** (`mode: 'form'`) — collects **non‑sensitive** data via a schema‑driven form. +- **URL** (`mode: 'url'`) — for sensitive data or secure web‑based flows (API keys, payments, OAuth). The client opens a URL in the browser. + +> [!IMPORTANT] +> Sensitive information **must not** be collected via form elicitation; always use URL elicitation or out‑of‑band flows for secrets. + +```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_elicitation" +server.registerTool( + 'collect-feedback', + { + description: 'Collect user feedback via a form', + inputSchema: z.object({}) + }, + async (_args, ctx): Promise => { + const result = await ctx.mcpReq.elicitInput({ + mode: 'form', + message: 'Please share your feedback:', + requestedSchema: { + type: 'object', + properties: { + rating: { + type: 'number', + title: 'Rating (1\u20135)', + minimum: 1, + maximum: 5 + }, + comment: { type: 'string', title: 'Comment' } + }, + required: ['rating'] + } + }); + if (result.action === 'accept') { + return { + content: [ + { + type: 'text', + text: `Thanks! ${JSON.stringify(result.content)}` + } + ] + }; + } + return { content: [{ type: 'text', text: 'Feedback declined.' }] }; + } +); +``` + +> [!NOTE] +> For runnable examples, see [`elicitationFormExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationFormExample.ts) (form mode) and [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationUrlExample.ts) (URL mode). +> +> For protocol details, see [Elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation) in the MCP specification. + +## Tasks (experimental) + +Task-based execution enables "call-now, fetch-later" patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks: + +- Provide a {@linkcode @modelcontextprotocol/server!index.TaskStore | TaskStore} implementation that persists task metadata and results (see {@linkcode @modelcontextprotocol/server!index.InMemoryTaskStore | InMemoryTaskStore} for reference). +- Enable the `tasks` capability when constructing the server. +- Register tools with {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | server.experimental.tasks.registerToolTask(...)}. + +> [!NOTE] +> For a full runnable example, see [`simpleTaskInteractive.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleTaskInteractive.ts). + +> [!WARNING] +> The tasks API is experimental and may change without notice. + +## Deployment + +### DNS rebinding protection + +MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` from `@modelcontextprotocol/express` to create an Express app with DNS rebinding protection enabled by default: + +```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_basic" +// Default: DNS rebinding protection auto-enabled (host is 127.0.0.1) +const app = createMcpExpressApp(); + +// DNS rebinding protection also auto-enabled for localhost +const appLocal = createMcpExpressApp({ host: 'localhost' }); + +// No automatic protection when binding to all interfaces +const appOpen = createMcpExpressApp({ host: '0.0.0.0' }); +``` + +When binding to `0.0.0.0` / `::`, provide an allow-list of hosts: + +```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_allowedHosts" +const app = createMcpExpressApp({ + host: '0.0.0.0', + allowedHosts: ['localhost', '127.0.0.1', 'myhost.local'] +}); +``` ## More server features -The sections above cover the essentials. The SDK supports several additional capabilities — each is demonstrated in the runnable examples and covered in more detail in the linked references. +The sections above cover the essentials. The table below links to additional capabilities demonstrated in the runnable examples. | Feature | Description | Reference | |---------|-------------|-----------| @@ -349,8 +470,4 @@ The sections above cover the essentials. The SDK supports several additional cap | Session management | Per-session transport routing, initialization, and cleanup | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) | | Resumability | Replay missed SSE events via an event store | [`inMemoryEventStore.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/inMemoryEventStore.ts) | | CORS | Expose MCP headers (`mcp-session-id`, etc.) for browser clients | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) | -| Tool annotations | Hint whether tools are read-only, destructive, etc. | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) | -| Elicitation | Request user input (forms or URLs) during tool execution | [Capabilities guide](capabilities.md#elicitation), [`elicitationFormExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationFormExample.ts) | -| Sampling | Request LLM completions from the connected client | [Capabilities guide](capabilities.md#sampling), [`toolWithSampleServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/toolWithSampleServer.ts) | -| Tasks (experimental) | Long-running operations with polling and resumption | [Capabilities guide](capabilities.md#task-based-execution-experimental), [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) | | Multi‑node deployment | Stateless, persistent‑storage, and distributed routing patterns | [`examples/server/README.md`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/README.md#multi-node-deployment-patterns) | diff --git a/examples/client-quickstart/.gitignore b/examples/client-quickstart/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/examples/client-quickstart/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/client-quickstart/package.json b/examples/client-quickstart/package.json new file mode 100644 index 000000000..98919df99 --- /dev/null +++ b/examples/client-quickstart/package.json @@ -0,0 +1,21 @@ +{ + "name": "@modelcontextprotocol/examples-client-quickstart", + "private": true, + "version": "2.0.0-alpha.0", + "type": "module", + "bin": { + "mcp-client-cli": "./build/index.js" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", + "@modelcontextprotocol/client": "workspace:^" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "catalog:devTools" + } +} diff --git a/examples/client-quickstart/src/index.ts b/examples/client-quickstart/src/index.ts new file mode 100644 index 000000000..7836c9e00 --- /dev/null +++ b/examples/client-quickstart/src/index.ts @@ -0,0 +1,187 @@ +//#region prelude +import Anthropic from '@anthropic-ai/sdk'; +import { Client, StdioClientTransport, type CallToolResult } from '@modelcontextprotocol/client'; +import readline from 'readline/promises'; + +const ANTHROPIC_MODEL = 'claude-sonnet-4-5'; + +class MCPClient { + private mcp: Client; + private _anthropic: Anthropic | null = null; + private transport: StdioClientTransport | null = null; + private tools: Anthropic.Tool[] = []; + + constructor() { + // Initialize MCP client + this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' }); + } + + private get anthropic(): Anthropic { + // Lazy-initialize Anthropic client when needed + return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + } +//#endregion prelude + +//#region connectToServer + async connectToServer(serverScriptPath: string) { + try { + // Determine script type and appropriate command + const isJs = serverScriptPath.endsWith('.js'); + const isPy = serverScriptPath.endsWith('.py'); + if (!isJs && !isPy) { + throw new Error('Server script must be a .js or .py file'); + } + const command = isPy + ? (process.platform === 'win32' ? 'python' : 'python3') + : process.execPath; + + // Initialize transport and connect to server + this.transport = new StdioClientTransport({ command, args: [serverScriptPath] }); + await this.mcp.connect(this.transport); + + // List available tools + const toolsResult = await this.mcp.listTools(); + this.tools = toolsResult.tools.map((tool) => ({ + name: tool.name, + description: tool.description ?? '', + input_schema: tool.inputSchema as Anthropic.Tool.InputSchema, + })); + console.log('Connected to server with tools:', this.tools.map(({ name }) => name)); + } catch (e) { + console.log('Failed to connect to MCP server: ', e); + throw e; + } + } +//#endregion connectToServer + +//#region processQuery + async processQuery(query: string) { + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: query, + }, + ]; + + // Initial Claude API call + const response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); + + // Process response and handle tool calls + const finalText = []; + + for (const content of response.content) { + if (content.type === 'text') { + finalText.push(content.text); + } else if (content.type === 'tool_use') { + // Execute tool call + const toolName = content.name; + const toolArgs = content.input as Record | undefined; + const result = await this.mcp.callTool({ + name: toolName, + arguments: toolArgs, + }) as CallToolResult; + + finalText.push(`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`); + + // Extract text from tool result content blocks + const toolResultText = result.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + + // Continue conversation with tool results + messages.push({ + role: 'assistant', + content: response.content, + }); + messages.push({ + role: 'user', + content: [{ + type: 'tool_result', + tool_use_id: content.id, + content: toolResultText, + }], + }); + + // Get next response from Claude + const followUp = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + }); + + finalText.push(followUp.content[0].type === 'text' ? followUp.content[0].text : ''); + } + } + + return finalText.join('\n'); + } +//#endregion processQuery + +//#region chatLoop + async chatLoop() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + console.log('\nMCP Client Started!'); + console.log('Type your queries or "quit" to exit.'); + + while (true) { + const message = await rl.question('\nQuery: '); + if (message.toLowerCase() === 'quit') { + break; + } + const response = await this.processQuery(message); + console.log('\n' + response); + } + } finally { + rl.close(); + } + } + + async cleanup() { + await this.mcp.close(); + } +} +//#endregion chatLoop + +//#region main +async function main() { + if (process.argv.length < 3) { + console.log('Usage: node build/index.js '); + return; + } + const mcpClient = new MCPClient(); + try { + await mcpClient.connectToServer(process.argv[2]); + + // Check if we have a valid API key to continue + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + console.log( + '\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:' + + '\n export ANTHROPIC_API_KEY=your-api-key-here' + ); + return; + } + + await mcpClient.chatLoop(); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await mcpClient.cleanup(); + process.exit(0); + } +} + +main(); +//#endregion main diff --git a/examples/client-quickstart/tsconfig.json b/examples/client-quickstart/tsconfig.json new file mode 100644 index 000000000..9c229e5fe --- /dev/null +++ b/examples/client-quickstart/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], + "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/examples/server-quickstart/.gitignore b/examples/server-quickstart/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/examples/server-quickstart/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/server-quickstart/package.json b/examples/server-quickstart/package.json new file mode 100644 index 000000000..133af7a1d --- /dev/null +++ b/examples/server-quickstart/package.json @@ -0,0 +1,21 @@ +{ + "name": "@modelcontextprotocol/examples-server-quickstart", + "private": true, + "version": "2.0.0-alpha.0", + "type": "module", + "bin": { + "weather": "./build/index.js" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/server": "workspace:^", + "zod": "catalog:runtimeShared" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "catalog:devTools" + } +} diff --git a/examples/server-quickstart/src/index.ts b/examples/server-quickstart/src/index.ts new file mode 100644 index 000000000..dd42901fd --- /dev/null +++ b/examples/server-quickstart/src/index.ts @@ -0,0 +1,221 @@ +//#region prelude +import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server'; +import * as z from 'zod/v4'; + +const NWS_API_BASE = 'https://api.weather.gov'; +const USER_AGENT = 'weather-app/1.0'; + +// Create server instance +const server = new McpServer({ + name: 'weather', + version: '1.0.0', +}); +//#endregion prelude + +//#region helpers +// Helper function for making NWS API requests +async function makeNWSRequest(url: string): Promise { + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/geo+json', + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error('Error making NWS request:', error); + return null; + } +} + +interface AlertFeature { + properties: { + event?: string; + areaDesc?: string; + severity?: string; + status?: string; + headline?: string; + }; +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || 'Unknown'}`, + `Area: ${props.areaDesc || 'Unknown'}`, + `Severity: ${props.severity || 'Unknown'}`, + `Status: ${props.status || 'Unknown'}`, + `Headline: ${props.headline || 'No headline'}`, + '---', + ].join('\n'); +} + +interface ForecastPeriod { + name?: string; + temperature?: number; + temperatureUnit?: string; + windSpeed?: string; + windDirection?: string; + shortForecast?: string; +} + +interface AlertsResponse { + features: AlertFeature[]; +} + +interface PointsResponse { + properties: { + forecast?: string; + }; +} + +interface ForecastResponse { + properties: { + periods: ForecastPeriod[]; + }; +} +//#endregion helpers + +//#region registerTools +// Register weather tools +server.registerTool( + 'get-alerts', + { + title: 'Get Weather Alerts', + description: 'Get weather alerts for a state', + inputSchema: z.object({ + state: z.string().length(2) + .describe('Two-letter state code (e.g. CA, NY)'), + }), + }, + async ({ state }) => { + const stateCode = state.toUpperCase(); + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest(alertsUrl); + + if (!alertsData) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to retrieve alerts data', + }], + }; + } + + const features = alertsData.features || []; + + if (features.length === 0) { + return { + content: [{ + type: 'text' as const, + text: `No active alerts for ${stateCode}`, + }], + }; + } + + const formattedAlerts = features.map(formatAlert); + + return { + content: [{ + type: 'text' as const, + text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`, + }], + }; + }, +); + +server.registerTool( + 'get-forecast', + { + title: 'Get Weather Forecast', + description: 'Get weather forecast for a location', + inputSchema: z.object({ + latitude: z.number().min(-90).max(90) + .describe('Latitude of the location'), + longitude: z.number().min(-180).max(180) + .describe('Longitude of the location'), + }), + }, + async ({ latitude, longitude }) => { + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest(pointsUrl); + + if (!pointsData) { + return { + content: [{ + type: 'text' as const, + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to get forecast URL from grid point data', + }], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest(forecastUrl); + if (!forecastData) { + return { + content: [{ + type: 'text' as const, + text: 'Failed to retrieve forecast data', + }], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [{ + type: 'text' as const, + text: 'No forecast periods available', + }], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || 'Unknown'}:`, + `Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`, + `Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`, + `${period.shortForecast || 'No forecast available'}`, + '---', + ].join('\n'), + ); + + return { + content: [{ + type: 'text' as const, + text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`, + }], + }; + }, +); +//#endregion registerTools + +//#region main +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Weather MCP Server running on stdio'); +} + +main().catch((error) => { + console.error('Fatal error in main():', error); + process.exit(1); +}); +//#endregion main diff --git a/examples/server-quickstart/tsconfig.json b/examples/server-quickstart/tsconfig.json new file mode 100644 index 000000000..9fdefa15e --- /dev/null +++ b/examples/server-quickstart/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/examples/server/src/serverGuide.examples.ts b/examples/server/src/serverGuide.examples.ts index 271d0d39b..ca4be3d4a 100644 --- a/examples/server/src/serverGuide.examples.ts +++ b/examples/server/src/serverGuide.examples.ts @@ -207,6 +207,88 @@ function registerTool_logging() { return server; } +// --------------------------------------------------------------------------- +// Server-initiated requests +// --------------------------------------------------------------------------- + +/** Example: Tool that uses sampling to request an LLM completion from the client. */ +function registerTool_sampling(server: McpServer) { + //#region registerTool_sampling + server.registerTool( + 'summarize', + { + description: 'Summarize text using the client LLM', + inputSchema: z.object({ text: z.string() }) + }, + async ({ text }, ctx): Promise => { + const response = await ctx.mcpReq.requestSampling({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please summarize:\n\n${text}` + } + } + ], + maxTokens: 500 + }); + return { + content: [ + { + type: 'text', + text: `Model (${response.model}): ${JSON.stringify(response.content)}` + } + ] + }; + } + ); + //#endregion registerTool_sampling +} + +/** Example: Tool that uses form elicitation to collect user input. */ +function registerTool_elicitation(server: McpServer) { + //#region registerTool_elicitation + server.registerTool( + 'collect-feedback', + { + description: 'Collect user feedback via a form', + inputSchema: z.object({}) + }, + async (_args, ctx): Promise => { + const result = await ctx.mcpReq.elicitInput({ + mode: 'form', + message: 'Please share your feedback:', + requestedSchema: { + type: 'object', + properties: { + rating: { + type: 'number', + title: 'Rating (1\u20135)', + minimum: 1, + maximum: 5 + }, + comment: { type: 'string', title: 'Comment' } + }, + required: ['rating'] + } + }); + if (result.action === 'accept') { + return { + content: [ + { + type: 'text', + text: `Thanks! ${JSON.stringify(result.content)}` + } + ] + }; + } + return { content: [{ type: 'text', text: 'Feedback declined.' }] }; + } + ); + //#endregion registerTool_elicitation +} + // --------------------------------------------------------------------------- // Transports // --------------------------------------------------------------------------- @@ -294,6 +376,8 @@ function dnsRebinding_allowedHosts() { void registerTool_basic; void registerTool_resourceLink; void registerTool_logging; +void registerTool_sampling; +void registerTool_elicitation; void registerResource_static; void registerResource_template; void registerPrompt_basic; diff --git a/package.json b/package.json index 8c756f032..720950472 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "sync:snippets": "tsx scripts/sync-snippets.ts", "examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth", "docs": "typedoc", - "docs:check": "typedoc --emit none", + "docs:check": "typedoc", "typecheck:all": "pnpm -r typecheck", "build:all": "pnpm -r build", "prepack:all": "pnpm -r prepack", diff --git a/packages/core/src/experimental/tasks/stores/inMemory.ts b/packages/core/src/experimental/tasks/stores/inMemory.ts index 8af800029..c6c4a3015 100644 --- a/packages/core/src/experimental/tasks/stores/inMemory.ts +++ b/packages/core/src/experimental/tasks/stores/inMemory.ts @@ -31,6 +31,7 @@ export class InMemoryTaskStore implements TaskStore { return crypto.randomUUID().replaceAll('-', ''); } + /** {@inheritDoc TaskStore.createTask} */ async createTask(taskParams: CreateTaskOptions, requestId: RequestId, request: Request, sessionId?: string): Promise { // Generate a unique task ID const taskId = this.generateTaskId(); @@ -96,6 +97,7 @@ export class InMemoryTaskStore implements TaskStore { return stored ? { ...stored.task } : null; } + /** {@inheritDoc TaskStore.storeTaskResult} */ async storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise { const stored = this.getStoredTask(taskId, sessionId); if (!stored) { @@ -129,6 +131,7 @@ export class InMemoryTaskStore implements TaskStore { } } + /** {@inheritDoc TaskStore.getTaskResult} */ async getTaskResult(taskId: string, sessionId?: string): Promise { const stored = this.getStoredTask(taskId, sessionId); if (!stored) { @@ -142,6 +145,7 @@ export class InMemoryTaskStore implements TaskStore { return stored.result; } + /** {@inheritDoc TaskStore.updateTaskStatus} */ async updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise { const stored = this.getStoredTask(taskId, sessionId); if (!stored) { @@ -178,6 +182,7 @@ export class InMemoryTaskStore implements TaskStore { } } + /** {@inheritDoc TaskStore.listTasks} */ async listTasks(cursor?: string, sessionId?: string): Promise<{ tasks: Task[]; nextCursor?: string }> { const PAGE_SIZE = 10; diff --git a/packages/core/src/shared/responseMessage.ts b/packages/core/src/shared/responseMessage.ts index 49059489b..b776d853c 100644 --- a/packages/core/src/shared/responseMessage.ts +++ b/packages/core/src/shared/responseMessage.ts @@ -1,14 +1,17 @@ import type { Result, Task } from '../types/types.js'; /** - * Base message type + * Base message type for the response stream. */ export interface BaseResponseMessage { type: string; } /** - * Task status update message + * Task status update message. + * + * Yielded on each poll iteration while the task is active (e.g. while + * `working`). May be emitted multiple times with the same status. */ export interface TaskStatusMessage extends BaseResponseMessage { type: 'taskStatus'; @@ -16,7 +19,10 @@ export interface TaskStatusMessage extends BaseResponseMessage { } /** - * Task created message (first message for task-augmented requests) + * Task created message. + * + * Yielded once when the server creates a new task for a long-running operation. + * This is always the first message for task-augmented requests. */ export interface TaskCreatedMessage extends BaseResponseMessage { type: 'taskCreated'; @@ -24,7 +30,10 @@ export interface TaskCreatedMessage extends BaseResponseMessage { } /** - * Final result message (terminal) + * Final result message. + * + * Yielded once when the operation completes successfully. Terminal — no further + * messages will follow. */ export interface ResultMessage extends BaseResponseMessage { type: 'result'; @@ -32,7 +41,9 @@ export interface ResultMessage extends BaseResponseMessage { } /** - * Error message (terminal) + * Error message. + * + * Yielded once if the operation fails. Terminal — no further messages will follow. */ export interface ErrorMessage extends BaseResponseMessage { type: 'error'; @@ -40,14 +51,26 @@ export interface ErrorMessage extends BaseResponseMessage { } /** - * Union type representing all possible messages that can be yielded during request processing. - * Note: Progress notifications are handled through the existing {@linkcode index.RequestOptions | onprogress} callback mechanism. + * Union of all message types yielded by task-aware streaming APIs such as + * {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#callToolStream | callToolStream()}, + * {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#requestStream | ExperimentalClientTasks.requestStream()}, and + * {@linkcode @modelcontextprotocol/server!experimental/tasks/server.ExperimentalServerTasks#requestStream | ExperimentalServerTasks.requestStream()}. + * + * A typical sequence is: + * 1. `taskCreated` — task is registered (once) + * 2. `taskStatus` — zero or more progress updates + * 3. `result` **or** `error` — terminal message (once) + * + * Progress notifications are handled through the existing {@linkcode index.RequestOptions | onprogress} callback. * Side-channeled messages (server requests/notifications) are handled through registered handlers. */ export type ResponseMessage = TaskStatusMessage | TaskCreatedMessage | ResultMessage | ErrorMessage; export type AsyncGeneratorValue = T extends AsyncGenerator ? U : never; +/** + * Collects all values from an async generator into an array. + */ export async function toArrayAsync>(it: T): Promise[]> { const arr: AsyncGeneratorValue[] = []; for await (const o of it) { @@ -57,6 +80,11 @@ export async function toArrayAsync>(it: T): Pr return arr; } +/** + * Consumes a {@linkcode ResponseMessage} stream and returns the final result, + * discarding intermediate `taskCreated` and `taskStatus` messages. Throws + * if an `error` message is received or the stream ends without a result. + */ export async function takeResult>>(it: U): Promise { for await (const o of it) { if (o.type === 'result') { diff --git a/packages/middleware/express/README.md b/packages/middleware/express/README.md index 386141d14..99189aa20 100644 --- a/packages/middleware/express/README.md +++ b/packages/middleware/express/README.md @@ -2,7 +2,7 @@ Express adapters for the MCP TypeScript server SDK. -This package is a thin Express integration layer for [`@modelcontextprotocol/server`](../../server/). +This package is a thin Express integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It does **not** implement MCP itself. Instead, it helps you: diff --git a/packages/middleware/hono/README.md b/packages/middleware/hono/README.md index a7339bf68..f59188850 100644 --- a/packages/middleware/hono/README.md +++ b/packages/middleware/hono/README.md @@ -2,7 +2,7 @@ Hono adapters for the MCP TypeScript server SDK. -This package is a thin Hono integration layer for [`@modelcontextprotocol/server`](../../server/). +This package is a thin Hono integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It does **not** implement MCP itself. Instead, it helps you: diff --git a/packages/middleware/node/README.md b/packages/middleware/node/README.md index 678e1d452..fe10c9f2a 100644 --- a/packages/middleware/node/README.md +++ b/packages/middleware/node/README.md @@ -2,7 +2,7 @@ Node.js adapters for the MCP TypeScript server SDK. -This package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](../../server/). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`. +This package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`. For web‑standard runtimes (Cloudflare Workers, Deno, Bun, etc.), use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly. diff --git a/packages/server/src/experimental/tasks/interfaces.ts b/packages/server/src/experimental/tasks/interfaces.ts index b80f1f5a4..d12ee11d8 100644 --- a/packages/server/src/experimental/tasks/interfaces.ts +++ b/packages/server/src/experimental/tasks/interfaces.ts @@ -41,10 +41,27 @@ export type TaskRequestHandler { + /** + * Called on the initial `tools/call` request. + * + * Creates a task via `ctx.task.store.createTask(...)`, starts any + * background work, and returns the task object. + */ createTask: CreateTaskRequestHandler; + /** + * Handler for `tasks/get` requests. + */ getTask: TaskRequestHandler; + /** + * Handler for `tasks/result` requests. + */ getTaskResult: TaskRequestHandler; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2099eab0f..3150f1648 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,22 @@ importers: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) + examples/client-quickstart: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.74.0 + version: 0.74.0(zod@4.3.5) + '@modelcontextprotocol/client': + specifier: workspace:^ + version: link:../../packages/client + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + typescript: + specifier: catalog:devTools + version: 5.9.3 + examples/server: dependencies: '@hono/node-server': @@ -367,6 +383,22 @@ importers: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) + examples/server-quickstart: + dependencies: + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../packages/server + zod: + specifier: catalog:runtimeShared + version: 4.3.5 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + typescript: + specifier: catalog:devTools + version: 5.9.3 + examples/shared: dependencies: '@modelcontextprotocol/core': @@ -943,6 +975,15 @@ importers: packages: + '@anthropic-ai/sdk@0.74.0': + resolution: {integrity: sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -3489,6 +3530,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4187,6 +4232,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -4535,6 +4583,12 @@ packages: snapshots: + '@anthropic-ai/sdk@0.74.0(zod@4.3.5)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.5 + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -6965,6 +7019,11 @@ snapshots: json-buffer@3.0.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.4 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -7746,6 +7805,8 @@ snapshots: tree-kill@1.2.2: {} + ts-algebra@2.0.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 diff --git a/scripts/sync-snippets.ts b/scripts/sync-snippets.ts index fb91eee1c..21a2c4e70 100644 --- a/scripts/sync-snippets.ts +++ b/scripts/sync-snippets.ts @@ -209,8 +209,6 @@ function findLabeledCodeFences( * @returns The dedented content */ function dedent(content: string, baseIndent: string): string { - if (!baseIndent) return content; - const lines = content.split('\n'); const dedentedLines = lines.map((line) => { // Preserve empty lines as-is @@ -314,7 +312,7 @@ function getOrLoadRegion( if (fileContent === undefined) { try { - fileContent = readFileSync(absoluteExamplePath, 'utf-8').trim(); + fileContent = readFileSync(absoluteExamplePath, 'utf-8'); } catch { throw new Error(`Example file not found: ${absoluteExamplePath}`); } @@ -323,7 +321,7 @@ function getOrLoadRegion( // If no region name, return whole file if (!regionName) { - return fileContent; + return fileContent.trim(); } // Extract region from cached file content, cache the result diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 365619844..f58a3c4b4 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -31,6 +31,7 @@ export default { blockTags: [...OptionDefaults.blockTags, '@format'], exclude: ['**/*.examples.ts'] }, + highlightLanguages: [...OptionDefaults.highlightLanguages, 'powershell'], projectDocuments: ['docs/documents.md'], navigation: { compactFolders: true,