diff --git a/README.md b/README.md index 5776a96e..bd4789d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # MCP TypeScript SDK ![NPM Version](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk) ![MIT licensed](https://img.shields.io/npm/l/%40modelcontextprotocol%2Fsdk) -## Table of Contents +
+Table of Contents - [Overview](#overview) - [Installation](#installation) @@ -30,14 +31,16 @@ - [Contributing](#contributing) - [License](#license) +
+ ## 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(); +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 mcpServer.connect(transport); - console.error('MCP server is running...'); -} + await transport.handleRequest(req, res, req.body); +}); -main().catch(error => { +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 remote servers, use the Streamable HTTP transport. -For command-line tools and direct integrations: +#### 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. -### Streamable HTTP + 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 + }); + } + } +}); -For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications. +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); +}); +``` #### 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 }; } );