@l10nmonster/mcp 3.0.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -0
- package/index.js +15 -0
- package/package.json +31 -0
- package/server.js +235 -0
- package/tests/integration.test.js +215 -0
- package/tests/mcpToolValidation.test.js +947 -0
- package/tests/registry.test.js +169 -0
- package/tools/index.js +3 -0
- package/tools/mcpTool.js +214 -0
- package/tools/registry.js +69 -0
- package/tools/sourceQuery.js +88 -0
- package/tools/status.js +665 -0
- package/tools/translate.js +227 -0
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# L10n Monster MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for L10n Monster, exposing translation management functionality to Claude Code and other MCP clients.
|
|
4
|
+
The server exposed an standards-compliant Streamable HTTP transport with per-request isolation.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
Register MCP as an extension to be served and then start an l10n monster server as usual.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Example
|
|
12
|
+
|
|
13
|
+
`l10nmonster.config.mjs`:
|
|
14
|
+
```javascript
|
|
15
|
+
import config from '@l10nmonster/core';
|
|
16
|
+
import serve from '@l10nmonster/server';
|
|
17
|
+
import { createMcpRoutes } from '@l10nmonster/mcp';
|
|
18
|
+
serve.registerExtension('mcp', createMcpRoutes);
|
|
19
|
+
|
|
20
|
+
export default config.l10nMonster(import.meta.dirname).action(serve)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With above config, you can start the l10n server via:
|
|
24
|
+
```shell
|
|
25
|
+
$ npx l10n serve
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The server then exposes an mcp at `http://localhost:9000/api/ext/mcp` which can be leveraged by any LLM agent.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Available Tools
|
|
32
|
+
|
|
33
|
+
- `status` - Get status of various l10nmonster subsystems including channels, projects, providers, language pairs, jobs, translation memory etc. The caller agent controls which sub-systems to include and the level of details to allow for more efficient use of the context.
|
|
34
|
+
- `source_query` - Query source content and translation memory.
|
|
35
|
+
- `translate` - Translate segments using configured providers.
|
|
36
|
+
|
|
37
|
+
## Extending MCP with Custom Tools
|
|
38
|
+
|
|
39
|
+
The MCP server supports a registration-based extensibility pattern, allowing external packages to add custom tools without modifying the core MCP package. This mirrors the `ServeAction.registerExtension` pattern used by the L10n Monster server.
|
|
40
|
+
|
|
41
|
+
### Creating a Custom Tool
|
|
42
|
+
|
|
43
|
+
Custom tools extend the `McpTool` base class and follow the same conventions as built-in tools:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import { McpTool, McpInputError } from '@l10nmonster/mcp';
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
|
|
49
|
+
export class MyCustomTool extends McpTool {
|
|
50
|
+
static metadata = {
|
|
51
|
+
name: 'my_custom_tool',
|
|
52
|
+
description: 'Does something custom with translation data',
|
|
53
|
+
inputSchema: z.object({
|
|
54
|
+
channelId: z.string().describe('Channel ID to process'),
|
|
55
|
+
option: z.string().optional().default('default').describe('Optional processing option')
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
static async execute(mm, args) {
|
|
60
|
+
// Access MonsterManager methods directly
|
|
61
|
+
const channel = mm.rm.getChannel(args.channelId);
|
|
62
|
+
|
|
63
|
+
if (!channel) {
|
|
64
|
+
throw new McpInputError(`Channel "${args.channelId}" not found`, {
|
|
65
|
+
hints: [`Available channels: ${mm.rm.channelIds.join(', ')}`]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Return structured data
|
|
70
|
+
return {
|
|
71
|
+
channelId: args.channelId,
|
|
72
|
+
result: 'processed',
|
|
73
|
+
option: args.option
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Registering Custom Tools
|
|
80
|
+
|
|
81
|
+
Register your custom tools in your `l10nmonster.config.mjs` before calling `serve.registerExtension`:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
import config from '@l10nmonster/core';
|
|
85
|
+
import serve from '@l10nmonster/server';
|
|
86
|
+
import { createMcpRoutes, registerTool } from '@l10nmonster/mcp';
|
|
87
|
+
import { MyCustomTool } from './my-custom-tool.js';
|
|
88
|
+
|
|
89
|
+
// Register custom MCP tools
|
|
90
|
+
registerTool(MyCustomTool);
|
|
91
|
+
|
|
92
|
+
// Register MCP routes with the server
|
|
93
|
+
serve.registerExtension('mcp', createMcpRoutes);
|
|
94
|
+
|
|
95
|
+
export default config.l10nMonster(import.meta.dirname)
|
|
96
|
+
.action(serve);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Tool Registration in Helper Packages
|
|
100
|
+
|
|
101
|
+
Helper packages can export their MCP tools for registration by consumers:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// In @l10nmonster/helpers-custom/index.js
|
|
105
|
+
export { MyCustomTool } from './mcpTools/MyCustomTool.js';
|
|
106
|
+
|
|
107
|
+
// In l10nmonster.config.mjs
|
|
108
|
+
import { registerTool } from '@l10nmonster/mcp';
|
|
109
|
+
import { MyCustomTool } from '@l10nmonster/helpers-custom';
|
|
110
|
+
|
|
111
|
+
registerTool(MyCustomTool);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Tool Override
|
|
115
|
+
|
|
116
|
+
Registered tools can override built-in tools by using the same tool name. This allows customization of default behavior:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
import { McpTool } from '@l10nmonster/mcp';
|
|
120
|
+
import { z } from 'zod';
|
|
121
|
+
|
|
122
|
+
// Override the built-in 'status' tool with custom implementation
|
|
123
|
+
export class CustomStatusTool extends McpTool {
|
|
124
|
+
static metadata = {
|
|
125
|
+
name: 'status', // Same name as built-in tool
|
|
126
|
+
description: 'Custom status implementation',
|
|
127
|
+
inputSchema: z.object({
|
|
128
|
+
// Custom schema
|
|
129
|
+
})
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
static async execute(mm, args) {
|
|
133
|
+
// Custom implementation
|
|
134
|
+
return { custom: true };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
registerTool(CustomStatusTool);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Exported API
|
|
142
|
+
|
|
143
|
+
The MCP package exports the following for extensibility:
|
|
144
|
+
|
|
145
|
+
- **`registerTool(ToolClass)`** - Register a custom MCP tool
|
|
146
|
+
- **`McpTool`** - Base class for creating tools
|
|
147
|
+
- **Error types** for structured error handling:
|
|
148
|
+
- `McpToolError` - Base error with structured metadata
|
|
149
|
+
- `McpInputError` - Invalid input errors
|
|
150
|
+
- `McpNotFoundError` - Resource not found errors
|
|
151
|
+
- `McpProviderError` - Translation provider errors
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Examples
|
|
155
|
+
|
|
156
|
+
The `examples/` directory contains complete examples of custom MCP tools:
|
|
157
|
+
|
|
158
|
+
- **`custom-tool-example.js`** - Demonstrates creating custom tools including:
|
|
159
|
+
- `ProjectStatsTool` - Get detailed statistics for a specific project
|
|
160
|
+
- `QualityInsightsTool` - Analyze translation quality distribution
|
|
161
|
+
|
|
162
|
+
These examples show best practices for:
|
|
163
|
+
- Extending the `McpTool` base class
|
|
164
|
+
- Defining input schemas with Zod
|
|
165
|
+
- Accessing MonsterManager APIs
|
|
166
|
+
- Handling errors with structured error types
|
|
167
|
+
- Returning well-formatted results
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
When developing, the best way to test the MCP server is by running it and using the inspector.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
npx @modelcontextprotocol/inspector
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Creating New Tools
|
|
179
|
+
|
|
180
|
+
All tools live under `/tools` directory. Each tool interface directly with MonsterManager, providing structured access to L10n Monster functionality through the Model Context Protocol. Unlike CLI actions, these tools return structured data optimized for programmatic consumption rather than console output.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
All MCP tools inherit from the `McpTool` base class, which handles schema validation with Zod, automatic error formatting, and MCP response serialization. Tools are automatically discovered and registered at server startup by scanning exports from `tools/index.js`.
|
|
184
|
+
|
|
185
|
+
New tools should focus on a single responsibility while remaining composable with existing tools. Design them to be idempotent where possible, especially for query operations that shouldn't modify state.
|
|
186
|
+
|
|
187
|
+
Use consistent naming conventions: prefer verb-noun patterns like `source_query` or `translate_segments`, and standardize parameter names such as `channelId`, `sourceLang`, and `targetLang`. Write clear descriptions for both the tool and its parameters, as these directly influence how AI agents discover and use your tools. Include example values in parameter descriptions when they help clarify expected formats.
|
|
188
|
+
|
|
189
|
+
Remember that tool description and schema descriptions serve different purposes and audiences. Descriptions help with tool selection and understanding, while schema descriptions guide proper usage. So
|
|
190
|
+
- Aim to have good sensible default for variables to make it easy out-of-the-box usage. Users should be able to get started without extensive configuration.
|
|
191
|
+
- In case of error emit a helpful message to the caller with information to potentially recover. For example if a provided parameter is not valid in the error return a list of valid values so LLM can recover.
|
|
192
|
+
- Optional paramter should have their default explained.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
Additional reading:
|
|
197
|
+
- https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1382
|
|
198
|
+
- https://steipete.me/posts/2025/mcp-best-practices#tool--parameter-descriptions
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
### Schema Design
|
|
203
|
+
|
|
204
|
+
Define input schemas using Zod with descriptive field documentation. Use `.describe()` to explain each parameter's purpose and format, and leverage Zod's validation features like `.min()`, `.max()`, and `.optional()` to enforce constraints and provide sensible defaults.
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
inputSchema: z.object({
|
|
208
|
+
channelId: z.string().describe('Channel ID to fetch source TUs from'),
|
|
209
|
+
guids: z.array(z.string()).min(1).describe('Array of TU GUIDs to translate'),
|
|
210
|
+
whereCondition: z.string().optional().default('true').describe('SQL WHERE condition against sources')
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Implementation
|
|
215
|
+
|
|
216
|
+
Create a tool class that extends `McpTool` with static `metadata` and `execute` method:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
export class MyNewTool extends McpTool {
|
|
220
|
+
static metadata = {
|
|
221
|
+
name: 'my_new_tool',
|
|
222
|
+
description: 'Tool description for MCP discovery',
|
|
223
|
+
inputSchema: z.object({...})
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
static async execute(mm, args) {
|
|
227
|
+
// Call MonsterManager methods directly
|
|
228
|
+
// Return structured data - the base class handles MCP formatting
|
|
229
|
+
return { ... };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Export the tool from `tools/index.js` to make it available for automatic registration:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
export { MyNewTool } from './MyNewTool.js';
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Output Guidelines
|
|
241
|
+
|
|
242
|
+
Return structured objects rather than formatted strings, letting MCP clients handle presentation. Use arrays of objects for lists instead of concatenated strings, and include contextual metadata with results to help consumers understand what they're receiving.
|
|
243
|
+
|
|
244
|
+
The base class automatically formats results for MCP compatibility, so focus on returning clean, structured data that represents your tool's output naturally.
|
|
245
|
+
|
|
246
|
+
### Error Handling
|
|
247
|
+
|
|
248
|
+
The base class provides structured error handling with specific error types (`McpInputError`, `McpNotFoundError`, `McpProviderError`) that include machine-readable codes, retry hints, and detailed context. Let errors bubble up naturally unless you need to add domain-specific context.
|
|
249
|
+
|
|
250
|
+
When catching errors, use the structured error types to provide actionable information:
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
try {
|
|
254
|
+
return await someOperation();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new McpInputError(`Invalid channel: ${channelId}`, {
|
|
257
|
+
hints: ['Call translation_status to see available channels'],
|
|
258
|
+
cause: error
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Main MCP server integration
|
|
2
|
+
export { createMcpRoutes } from './server.js';
|
|
3
|
+
|
|
4
|
+
// Tool registration for extensibility
|
|
5
|
+
import { registry } from './tools/registry.js';
|
|
6
|
+
export const registerTool = registry.registerTool.bind(registry);
|
|
7
|
+
|
|
8
|
+
// Base classes and utilities for creating custom tools
|
|
9
|
+
export {
|
|
10
|
+
McpTool,
|
|
11
|
+
McpToolError,
|
|
12
|
+
McpInputError,
|
|
13
|
+
McpNotFoundError,
|
|
14
|
+
McpProviderError
|
|
15
|
+
} from './tools/mcpTool.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@l10nmonster/mcp",
|
|
3
|
+
"version": "3.0.0-alpha.16",
|
|
4
|
+
"description": "L10n Monster Model Context Protocol (MCP) Server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test tests/*.test.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.18.1",
|
|
12
|
+
"zod": "^3"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@l10nmonster/core": "^3.0.0-alpha.0"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.11.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"localization",
|
|
26
|
+
"translation",
|
|
27
|
+
"l10n"
|
|
28
|
+
],
|
|
29
|
+
"author": "L10n Monster",
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import * as mcpTools from './tools/index.js';
|
|
6
|
+
import { registry } from './tools/registry.js';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// Session management for HTTP transport
|
|
11
|
+
const sessions = new Map(); // sessionId -> { transport, lastActivity }
|
|
12
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
13
|
+
|
|
14
|
+
// Shared MCP server instance per MonsterManager
|
|
15
|
+
// Use a WeakMap to avoid memory leaks. Once all sessions to a server are closed the server will be garbage collected.
|
|
16
|
+
const serverInstances = new WeakMap(); // monsterManager -> McpServer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async function getMcpPackageVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const packageJsonContent = await readFile(path.join(import.meta.dirname, 'package.json'), 'utf-8');
|
|
22
|
+
const packageJson = JSON.parse(packageJsonContent.toString());
|
|
23
|
+
return packageJson.version;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error parsing MCP package version:', error);
|
|
26
|
+
return '0.0.1-unknown';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Set server version to be the package version
|
|
31
|
+
const serverVersion = await getMcpPackageVersion();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Setup tools on an MCP server instance
|
|
35
|
+
*
|
|
36
|
+
* Registers all MCP tools from the registry, including:
|
|
37
|
+
* 1. Built-in tools (auto-registered from ./tools/index.js)
|
|
38
|
+
* 2. External tools registered via registerTool()
|
|
39
|
+
*/
|
|
40
|
+
async function setupToolsOnServer(server, monsterManager) {
|
|
41
|
+
// Register built-in tools if not already registered
|
|
42
|
+
const builtInTools = Object.values(mcpTools).filter(ToolClass => (
|
|
43
|
+
ToolClass &&
|
|
44
|
+
typeof ToolClass === 'function' &&
|
|
45
|
+
typeof ToolClass.handler === 'function' &&
|
|
46
|
+
ToolClass.metadata));
|
|
47
|
+
|
|
48
|
+
for (const ToolClass of builtInTools) {
|
|
49
|
+
const toolName = ToolClass.metadata.name;
|
|
50
|
+
if (!registry.hasTool(toolName)) {
|
|
51
|
+
registry.registerTool(ToolClass);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Register all tools from the registry with the MCP server
|
|
56
|
+
for (const ToolClass of registry.getAllTools()) {
|
|
57
|
+
const { name, description, inputSchema } = ToolClass.metadata;
|
|
58
|
+
const handler = ToolClass.handler(monsterManager);
|
|
59
|
+
|
|
60
|
+
console.info(`Registering MCP tool: ${name}`);
|
|
61
|
+
await server.registerTool(
|
|
62
|
+
name,
|
|
63
|
+
{
|
|
64
|
+
title: name,
|
|
65
|
+
description,
|
|
66
|
+
inputSchema: inputSchema.shape,
|
|
67
|
+
},
|
|
68
|
+
handler,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get or create a shared MCP server instance for a MonsterManager
|
|
75
|
+
*/
|
|
76
|
+
async function getOrCreateSharedServer(monsterManager) {
|
|
77
|
+
let server = serverInstances.get(monsterManager);
|
|
78
|
+
|
|
79
|
+
if (!server) {
|
|
80
|
+
server = new McpServer({
|
|
81
|
+
name: 'l10nmonster-mcp',
|
|
82
|
+
version: serverVersion,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await setupToolsOnServer(server, monsterManager);
|
|
86
|
+
serverInstances.set(monsterManager, server);
|
|
87
|
+
console.info('Created shared MCP server instance');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return server;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clean up expired sessions on-demand
|
|
95
|
+
*/
|
|
96
|
+
async function cleanupExpiredSessions() {
|
|
97
|
+
let cleaned = 0;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
for (const [sessionId, session] of sessions.entries()) {
|
|
100
|
+
if (now - session.lastActivity > SESSION_TIMEOUT_MS) {
|
|
101
|
+
try {
|
|
102
|
+
console.info(`Cleaning up expired session: ${sessionId}`);
|
|
103
|
+
await session.transport.close();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Log error per session but continue cleaning other expired sessions
|
|
106
|
+
console.error(`Error cleaning up expired session ${sessionId}:`, error);
|
|
107
|
+
} finally {
|
|
108
|
+
sessions.delete(sessionId);
|
|
109
|
+
cleaned++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return cleaned;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates MCP route handlers for use with the serve action extension mechanism.
|
|
118
|
+
* Returns route definitions that can be registered via ServeAction.registerExtension.
|
|
119
|
+
*
|
|
120
|
+
* @param {import('@l10nmonster/core').MonsterManager} mm - MonsterManager instance
|
|
121
|
+
* @returns {Array<[string, string, Function]>} Array of [method, path, handler] route definitions
|
|
122
|
+
*/
|
|
123
|
+
export function createMcpRoutes(mm) {
|
|
124
|
+
// Handle POST requests for client-to-server communication
|
|
125
|
+
const handlePost = async (req, res) => {
|
|
126
|
+
// Clean up expired sessions on each request
|
|
127
|
+
await cleanupExpiredSessions();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
131
|
+
let session;
|
|
132
|
+
|
|
133
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
134
|
+
// Existing session - update activity timestamp
|
|
135
|
+
session = sessions.get(sessionId);
|
|
136
|
+
session.lastActivity = Date.now();
|
|
137
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
138
|
+
// New initialization request - create transport and connect shared server
|
|
139
|
+
const transport = new StreamableHTTPServerTransport({
|
|
140
|
+
sessionIdGenerator: () => randomUUID(),
|
|
141
|
+
onsessioninitialized: (newSessionId) => {
|
|
142
|
+
sessions.set(newSessionId, {
|
|
143
|
+
transport,
|
|
144
|
+
lastActivity: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
// DNS rebinding protection is disabled by default for backwards compatibility.
|
|
148
|
+
// For production use, consider enabling:
|
|
149
|
+
// enableDnsRebindingProtection: true,
|
|
150
|
+
// allowedHosts: ['127.0.0.1'],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
transport.onclose = () => {
|
|
154
|
+
if (transport.sessionId) {
|
|
155
|
+
console.info(`Session closed: ${transport.sessionId}`);
|
|
156
|
+
sessions.delete(transport.sessionId);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Get or create shared MCP server and connect to new transport
|
|
161
|
+
const server = await getOrCreateSharedServer(mm);
|
|
162
|
+
await server.connect(transport);
|
|
163
|
+
console.info(`New MCP session initialized: ${transport.sessionId}`);
|
|
164
|
+
|
|
165
|
+
session = { transport, lastActivity: Date.now() };
|
|
166
|
+
} else {
|
|
167
|
+
// Invalid request - no session ID and not an initialize request
|
|
168
|
+
return res.status(400).json({
|
|
169
|
+
jsonrpc: '2.0',
|
|
170
|
+
error: {
|
|
171
|
+
code: -32000,
|
|
172
|
+
message: 'Bad Request: No valid session ID provided and not an initialize request',
|
|
173
|
+
},
|
|
174
|
+
id: null,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle the request through the transport
|
|
179
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Error handling MCP POST request:', error);
|
|
182
|
+
res.status(500).json({
|
|
183
|
+
jsonrpc: '2.0',
|
|
184
|
+
error: {
|
|
185
|
+
code: -32603,
|
|
186
|
+
message: 'Internal server error',
|
|
187
|
+
data: { detail: error.message },
|
|
188
|
+
},
|
|
189
|
+
id: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Handle GET and DELETE requests for existing sessions
|
|
195
|
+
const handleSessionRequest = async (req, res) => {
|
|
196
|
+
// Clean up expired sessions on each request
|
|
197
|
+
await cleanupExpiredSessions();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
201
|
+
|
|
202
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
203
|
+
return res.status(400).json({
|
|
204
|
+
jsonrpc: '2.0',
|
|
205
|
+
error: {
|
|
206
|
+
code: -32000,
|
|
207
|
+
message: 'Invalid or missing session ID',
|
|
208
|
+
},
|
|
209
|
+
id: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const session = sessions.get(sessionId);
|
|
214
|
+
session.lastActivity = Date.now();
|
|
215
|
+
await session.transport.handleRequest(req, res);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Error handling MCP session request:', error);
|
|
218
|
+
res.status(500).json({
|
|
219
|
+
jsonrpc: '2.0',
|
|
220
|
+
error: {
|
|
221
|
+
code: -32603,
|
|
222
|
+
message: 'Internal server error',
|
|
223
|
+
data: { detail: error.message },
|
|
224
|
+
},
|
|
225
|
+
id: null,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return [
|
|
231
|
+
['post', '/', handlePost],
|
|
232
|
+
['get', '/', handleSessionRequest],
|
|
233
|
+
['delete', '/', handleSessionRequest],
|
|
234
|
+
];
|
|
235
|
+
}
|