|
| 1 | +/** |
| 2 | + * SEP-2792 i18n Example Server |
| 3 | + * |
| 4 | + * Demonstrates per-request language negotiation using the MCP i18n helpers. |
| 5 | + * Supports three languages (en, fr, de) and exposes a `get_greeting` tool |
| 6 | + * with localized title, description, and response content. |
| 7 | + * |
| 8 | + * Run via stdio: tsx src/i18nExample.ts stdio |
| 9 | + * Run via HTTP: tsx src/i18nExample.ts http |
| 10 | + */ |
| 11 | + |
| 12 | +import { createMcpExpressApp } from '@modelcontextprotocol/express'; |
| 13 | +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; |
| 14 | +import type { CallToolResult, ListToolsResult } from '@modelcontextprotocol/server'; |
| 15 | +import { ACCEPT_LANGUAGE_META, getAcceptLanguage, McpServer, negotiateLanguage, setContentLanguage } from '@modelcontextprotocol/server'; |
| 16 | +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; |
| 17 | +import * as z from 'zod/v4'; |
| 18 | + |
| 19 | +// ---------- Localization dictionaries ---------- |
| 20 | + |
| 21 | +const AVAILABLE_LANGUAGES = ['en', 'fr', 'de']; |
| 22 | + |
| 23 | +const STRINGS: Record<string, Record<string, string>> = { |
| 24 | + 'tool.get_greeting.title': { |
| 25 | + en: 'Get Greeting', |
| 26 | + fr: 'Obtenir un salut', |
| 27 | + de: 'Begrüßung erhalten' |
| 28 | + }, |
| 29 | + 'tool.get_greeting.description': { |
| 30 | + en: 'Returns a greeting in the negotiated language', |
| 31 | + fr: 'Retourne un salut dans la langue négociée', |
| 32 | + de: 'Gibt eine Begrüßung in der ausgehandelten Sprache zurück' |
| 33 | + }, |
| 34 | + greeting: { |
| 35 | + en: 'Hello, {name}! Welcome.', |
| 36 | + fr: 'Bonjour, {name} ! Bienvenue.', |
| 37 | + de: 'Hallo, {name}! Willkommen.' |
| 38 | + } |
| 39 | +}; |
| 40 | + |
| 41 | +function t(key: string, lang: string, replacements?: Record<string, string>): string { |
| 42 | + let template = STRINGS[key]?.[lang] ?? STRINGS[key]?.['en'] ?? key; |
| 43 | + if (!replacements) return template; |
| 44 | + for (const [k, v] of Object.entries(replacements)) { |
| 45 | + template = template.replace(`{${k}}`, v); |
| 46 | + } |
| 47 | + return template; |
| 48 | +} |
| 49 | + |
| 50 | +// ---------- Server setup ---------- |
| 51 | + |
| 52 | +function createI18nServer(): McpServer { |
| 53 | + const server = new McpServer( |
| 54 | + { |
| 55 | + name: 'i18n-example-server', |
| 56 | + version: '1.0.0' |
| 57 | + }, |
| 58 | + { capabilities: { tools: {} } } |
| 59 | + ); |
| 60 | + |
| 61 | + // Override tools/list to support per-request localized metadata |
| 62 | + server.server.setRequestHandler('tools/list', (request, ctx): ListToolsResult => { |
| 63 | + const acceptLang = ctx.mcpReq._meta?.[ACCEPT_LANGUAGE_META] as string | undefined; |
| 64 | + const lang = negotiateLanguage(acceptLang ?? '', AVAILABLE_LANGUAGES, 'en')!; |
| 65 | + |
| 66 | + const result: ListToolsResult = { |
| 67 | + tools: [ |
| 68 | + { |
| 69 | + name: 'get_greeting', |
| 70 | + title: t('tool.get_greeting.title', lang), |
| 71 | + description: t('tool.get_greeting.description', lang), |
| 72 | + inputSchema: { |
| 73 | + type: 'object' as const, |
| 74 | + properties: { |
| 75 | + name: { type: 'string', description: 'Name to greet' } |
| 76 | + }, |
| 77 | + required: ['name'] |
| 78 | + } |
| 79 | + } |
| 80 | + ] |
| 81 | + }; |
| 82 | + setContentLanguage(result, lang); |
| 83 | + return result; |
| 84 | + }); |
| 85 | + |
| 86 | + // Register the tool for tools/call via McpServer |
| 87 | + server.registerTool( |
| 88 | + 'get_greeting', |
| 89 | + { |
| 90 | + title: 'Get Greeting', |
| 91 | + description: 'Returns a greeting in the negotiated language', |
| 92 | + inputSchema: z.object({ |
| 93 | + name: z.string().describe('Name to greet') |
| 94 | + }) |
| 95 | + }, |
| 96 | + async ({ name }, ctx): Promise<CallToolResult> => { |
| 97 | + const acceptLang = getAcceptLanguage(ctx.mcpReq as { _meta?: Record<string, unknown> }) ?? ''; |
| 98 | + const lang = negotiateLanguage(acceptLang, AVAILABLE_LANGUAGES, 'en')!; |
| 99 | + |
| 100 | + const result: CallToolResult = { |
| 101 | + content: [ |
| 102 | + { |
| 103 | + type: 'text', |
| 104 | + text: t('greeting', lang, { name }) |
| 105 | + } |
| 106 | + ] |
| 107 | + }; |
| 108 | + setContentLanguage(result, lang); |
| 109 | + return result; |
| 110 | + } |
| 111 | + ); |
| 112 | + |
| 113 | + return server; |
| 114 | +} |
| 115 | + |
| 116 | +// ---------- Transport entry points ---------- |
| 117 | + |
| 118 | +// ---------- Main ---------- |
| 119 | + |
| 120 | +const mode = process.argv[2] || 'stdio'; |
| 121 | +if (mode === 'http') { |
| 122 | + const app = createMcpExpressApp(); |
| 123 | + |
| 124 | + app.post('/mcp', async (req, res) => { |
| 125 | + const server = createI18nServer(); |
| 126 | + const transport = new NodeStreamableHTTPServerTransport({ |
| 127 | + sessionIdGenerator: undefined // stateless |
| 128 | + }); |
| 129 | + await server.connect(transport); |
| 130 | + await transport.handleRequest(req, res, req.body); |
| 131 | + res.on('close', () => { |
| 132 | + transport.close(); |
| 133 | + server.close(); |
| 134 | + }); |
| 135 | + }); |
| 136 | + |
| 137 | + app.get('/mcp', (_req, res) => { |
| 138 | + res.writeHead(405).end( |
| 139 | + JSON.stringify({ |
| 140 | + jsonrpc: '2.0', |
| 141 | + error: { code: -32_000, message: 'Method not allowed.' }, |
| 142 | + id: null |
| 143 | + }) |
| 144 | + ); |
| 145 | + }); |
| 146 | + |
| 147 | + app.delete('/mcp', (_req, res) => { |
| 148 | + res.writeHead(405).end( |
| 149 | + JSON.stringify({ |
| 150 | + jsonrpc: '2.0', |
| 151 | + error: { code: -32_000, message: 'Method not allowed.' }, |
| 152 | + id: null |
| 153 | + }) |
| 154 | + ); |
| 155 | + }); |
| 156 | + |
| 157 | + const PORT = Number.parseInt(process.env.PORT ?? '3456', 10); |
| 158 | + app.listen(PORT, () => { |
| 159 | + console.error(`i18n example server running on http://localhost:${PORT}/mcp`); |
| 160 | + }); |
| 161 | +} else { |
| 162 | + const server = createI18nServer(); |
| 163 | + const transport = new StdioServerTransport(); |
| 164 | + await server.connect(transport); |
| 165 | + console.error('i18n example server running on stdio'); |
| 166 | +} |
0 commit comments