@ttoss/http-server-mcp 0.12.4 → 0.12.5
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/dist/index.cjs +319 -0
- package/dist/index.d.cts +749 -88
- package/dist/index.d.mts +905 -0
- package/dist/index.mjs +302 -0
- package/package.json +5 -5
- package/dist/esm/index.js +0 -205
- package/dist/index.d.ts +0 -244
- package/dist/index.js +0 -244
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
3
|
+
value: 'Module'
|
|
4
|
+
});
|
|
5
|
+
let node_async_hooks = require("node:async_hooks");
|
|
6
|
+
let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
7
|
+
let _ttoss_http_server = require("@ttoss/http-server");
|
|
8
|
+
let zod = require("zod");
|
|
9
|
+
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
10
|
+
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
const requestContextStore = new node_async_hooks.AsyncLocalStorage();
|
|
13
|
+
/**
|
|
14
|
+
* Generic HTTP helper for use inside MCP tool handlers.
|
|
15
|
+
*
|
|
16
|
+
* Accepts any full URL (third-party APIs, public APIs, etc.) or a path
|
|
17
|
+
* relative to the `apiBaseUrl` configured in `createMcpRouter`.
|
|
18
|
+
*
|
|
19
|
+
* Headers configured via `getApiHeaders` in `createMcpRouter` are injected
|
|
20
|
+
* automatically into every request, allowing transparent forwarding of auth
|
|
21
|
+
* tokens, API keys, or any other header — without coupling this helper to a
|
|
22
|
+
* specific authentication scheme. Per-call `options.headers` take precedence
|
|
23
|
+
* over context-injected headers.
|
|
24
|
+
*
|
|
25
|
+
* @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
|
|
26
|
+
* @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
|
|
27
|
+
* @param options - Optional body and per-call header overrides
|
|
28
|
+
* @returns Parsed JSON response body
|
|
29
|
+
*
|
|
30
|
+
* @example Bearer token forwarding (configured once in `createMcpRouter`)
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { apiCall, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
|
|
33
|
+
*
|
|
34
|
+
* // Tool handler – no manual auth wiring needed
|
|
35
|
+
* mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
|
|
36
|
+
* const data = await apiCall('GET', '/portfolios');
|
|
37
|
+
* return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* const mcpRouter = createMcpRouter(mcpServer, {
|
|
41
|
+
* apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
|
|
42
|
+
* // Forward the caller's Bearer token to every apiCall
|
|
43
|
+
* getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example x-api-key forwarding
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const mcpRouter = createMcpRouter(mcpServer, {
|
|
50
|
+
* apiBaseUrl: 'https://internal-service/api',
|
|
51
|
+
* getApiHeaders: (ctx) => ({
|
|
52
|
+
* 'x-api-key': ctx.headers['x-api-key'] as string,
|
|
53
|
+
* }),
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example Third-party or public API (full URL, no context required)
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
|
|
60
|
+
* const created = await apiCall('POST', 'https://api.example.com/items', {
|
|
61
|
+
* body: { name: 'widget' },
|
|
62
|
+
* headers: { Authorization: 'Bearer fixed-service-token' },
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
const apiCall = async (method, url, options) => {
|
|
67
|
+
const context = requestContextStore.getStore();
|
|
68
|
+
let resolvedUrl = url;
|
|
69
|
+
if (url.startsWith("/")) {
|
|
70
|
+
if (!context?.apiBaseUrl) throw new Error(`apiCall received a relative path ("${url}") but no apiBaseUrl is configured. Either pass a full URL or set apiBaseUrl in createMcpRouter options.`);
|
|
71
|
+
resolvedUrl = `${context.apiBaseUrl.replace(/\/$/, "")}${url}`;
|
|
72
|
+
}
|
|
73
|
+
const hasBody = options?.body !== void 0;
|
|
74
|
+
const headers = {
|
|
75
|
+
...(context !== void 0 ? context.apiHeaders : {}),
|
|
76
|
+
...(options?.headers ?? {})
|
|
77
|
+
};
|
|
78
|
+
const hasExplicitContentType = Object.keys(headers).some(headerName => {
|
|
79
|
+
return headerName.toLowerCase() === "content-type";
|
|
80
|
+
});
|
|
81
|
+
if (hasBody && !hasExplicitContentType) headers["Content-Type"] = "application/json";
|
|
82
|
+
const response = await fetch(resolvedUrl, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: hasBody ? JSON.stringify(options.body) : void 0
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const err = await response.json().catch(() => {
|
|
89
|
+
return {
|
|
90
|
+
error: response.statusText
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
throw new Error(err.error || `HTTP ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
if (response.status === 204 || response.status === 205) return;
|
|
96
|
+
if (response.headers.get("content-type")?.includes("application/json")) return response.json();
|
|
97
|
+
return response.text();
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Creates a Koa router configured to handle MCP protocol requests
|
|
101
|
+
*
|
|
102
|
+
* @param server - The MCP server instance with registered tools and resources
|
|
103
|
+
* @param options - Configuration options for the router
|
|
104
|
+
* @returns A Koa Router instance configured for MCP
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { App, bodyParser } from '@ttoss/http-server';
|
|
109
|
+
* import { createMcpRouter, McpServer, z } from '@ttoss/http-server-mcp';
|
|
110
|
+
*
|
|
111
|
+
* const mcpServer = new McpServer({
|
|
112
|
+
* name: 'my-server',
|
|
113
|
+
* version: '1.0.0',
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* mcpServer.registerTool(
|
|
117
|
+
* 'hello',
|
|
118
|
+
* {
|
|
119
|
+
* description: 'Say hello',
|
|
120
|
+
* inputSchema: { name: z.string() },
|
|
121
|
+
* },
|
|
122
|
+
* async ({ name }) => ({
|
|
123
|
+
* content: [{ type: 'text', text: `Hello, ${name}!` }],
|
|
124
|
+
* })
|
|
125
|
+
* );
|
|
126
|
+
*
|
|
127
|
+
* const app = new App();
|
|
128
|
+
* app.use(bodyParser());
|
|
129
|
+
*
|
|
130
|
+
* const mcpRouter = createMcpRouter(mcpServer);
|
|
131
|
+
* app.use(mcpRouter.routes());
|
|
132
|
+
*
|
|
133
|
+
* app.listen(3000);
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
const createMcpRouter = (server, options = {}) => {
|
|
137
|
+
const {
|
|
138
|
+
path = "/mcp",
|
|
139
|
+
sessionIdGenerator,
|
|
140
|
+
apiBaseUrl,
|
|
141
|
+
getApiHeaders
|
|
142
|
+
} = options;
|
|
143
|
+
const isStateful = sessionIdGenerator !== void 0;
|
|
144
|
+
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
|
|
145
|
+
let sharedTransport;
|
|
146
|
+
if (isStateful) {
|
|
147
|
+
sharedTransport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
|
|
148
|
+
sessionIdGenerator,
|
|
149
|
+
enableJsonResponse: true
|
|
150
|
+
});
|
|
151
|
+
server.connect(sharedTransport);
|
|
152
|
+
}
|
|
153
|
+
let statelessQueue = Promise.resolve();
|
|
154
|
+
const enqueueStateless = work => {
|
|
155
|
+
const result = statelessQueue.then(() => {
|
|
156
|
+
return work();
|
|
157
|
+
});
|
|
158
|
+
statelessQueue = result.catch(() => {});
|
|
159
|
+
return result;
|
|
160
|
+
};
|
|
161
|
+
const router = new _ttoss_http_server.Router();
|
|
162
|
+
const handleWithContext = async (ctx, body) => {
|
|
163
|
+
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
164
|
+
const runRequest = async transport => {
|
|
165
|
+
await transport.handleRequest(ctx.req, ctx.res, body);
|
|
166
|
+
ctx.respond = false;
|
|
167
|
+
};
|
|
168
|
+
if (isStateful && sharedTransport) {
|
|
169
|
+
if (needsContext) await requestContextStore.run({
|
|
170
|
+
apiBaseUrl,
|
|
171
|
+
apiHeaders
|
|
172
|
+
}, () => {
|
|
173
|
+
return runRequest(sharedTransport);
|
|
174
|
+
});else await runRequest(sharedTransport);
|
|
175
|
+
} else await enqueueStateless(async () => {
|
|
176
|
+
const transport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
|
|
177
|
+
sessionIdGenerator: void 0,
|
|
178
|
+
enableJsonResponse: true
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await server.connect(transport);
|
|
182
|
+
if (needsContext) await requestContextStore.run({
|
|
183
|
+
apiBaseUrl,
|
|
184
|
+
apiHeaders
|
|
185
|
+
}, () => {
|
|
186
|
+
return runRequest(transport);
|
|
187
|
+
});else await runRequest(transport);
|
|
188
|
+
} finally {
|
|
189
|
+
await transport.close();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
router.post(path, async ctx => {
|
|
194
|
+
try {
|
|
195
|
+
await handleWithContext(ctx, ctx.request.body);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!ctx.res.headersSent) {
|
|
198
|
+
ctx.status = 500;
|
|
199
|
+
ctx.body = {
|
|
200
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
router.delete(path, async ctx => {
|
|
206
|
+
try {
|
|
207
|
+
await handleWithContext(ctx);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (!ctx.res.headersSent) {
|
|
210
|
+
ctx.status = 500;
|
|
211
|
+
ctx.body = {
|
|
212
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return router;
|
|
218
|
+
};
|
|
219
|
+
const rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
|
|
220
|
+
const patchedServerSet = /* @__PURE__ */new WeakSet();
|
|
221
|
+
/**
|
|
222
|
+
* Registers a tool on an MCP server using a **plain JSON Schema** object for
|
|
223
|
+
* `inputSchema` instead of a Zod shape.
|
|
224
|
+
*
|
|
225
|
+
* This is useful when tool definitions are shared between the MCP server and an
|
|
226
|
+
* AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
|
|
227
|
+
* plain JSON Schema at runtime. Using this helper eliminates the lossy
|
|
228
|
+
* JSON-Schema→Zod conversion that would otherwise be required.
|
|
229
|
+
*
|
|
230
|
+
* Internally the helper:
|
|
231
|
+
* 1. Registers the tool via the standard `McpServer.registerTool` API using a
|
|
232
|
+
* Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
|
|
233
|
+
* `tools/call` handler correctly routes requests and delivers raw args to
|
|
234
|
+
* your handler.
|
|
235
|
+
* 2. Stores the original JSON Schema and patches the `tools/list` response
|
|
236
|
+
* handler so clients receive the verbatim JSON Schema over the wire.
|
|
237
|
+
*
|
|
238
|
+
* @param server - The `McpServer` instance to register the tool on.
|
|
239
|
+
* @param params - Tool configuration including name, description, inputSchema,
|
|
240
|
+
* and handler.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
|
|
245
|
+
*
|
|
246
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
247
|
+
*
|
|
248
|
+
* registerToolFromSchema(server, {
|
|
249
|
+
* name: 'get-project',
|
|
250
|
+
* description: 'Get a project by ID',
|
|
251
|
+
* inputSchema: {
|
|
252
|
+
* type: 'object',
|
|
253
|
+
* properties: { id: { type: 'string', description: 'Project public ID' } },
|
|
254
|
+
* required: ['id'],
|
|
255
|
+
* },
|
|
256
|
+
* handler: async ({ id }) => ({
|
|
257
|
+
* content: [{ type: 'text', text: `Project: ${id}` }],
|
|
258
|
+
* }),
|
|
259
|
+
* });
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
const registerToolFromSchema = (server, params) => {
|
|
263
|
+
const {
|
|
264
|
+
name,
|
|
265
|
+
description,
|
|
266
|
+
inputSchema = {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {}
|
|
269
|
+
},
|
|
270
|
+
handler
|
|
271
|
+
} = params;
|
|
272
|
+
if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
|
|
273
|
+
rawSchemaRegistryMap.get(server).set(name, inputSchema);
|
|
274
|
+
server.registerTool(name, {
|
|
275
|
+
description,
|
|
276
|
+
inputSchema: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
277
|
+
}, async args => {
|
|
278
|
+
return handler(args);
|
|
279
|
+
});
|
|
280
|
+
if (!patchedServerSet.has(server)) {
|
|
281
|
+
patchedServerSet.add(server);
|
|
282
|
+
const rawServer = server.server;
|
|
283
|
+
const origHandler = rawServer?._requestHandlers?.get("tools/list");
|
|
284
|
+
if (!origHandler && process.env.NODE_ENV !== "production") console.warn("[registerToolFromSchema] Could not patch tools/list — internal MCP SDK structure may have changed. The tool will still be callable, but tools/list may return a Zod-derived schema instead of the verbatim JSON Schema.");
|
|
285
|
+
if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
|
|
286
|
+
const result = await origHandler(rawRequest, extra);
|
|
287
|
+
const schemas = rawSchemaRegistryMap.get(server);
|
|
288
|
+
if (!schemas) return result;
|
|
289
|
+
return {
|
|
290
|
+
...result,
|
|
291
|
+
tools: result.tools.map(tool => {
|
|
292
|
+
const raw = schemas.get(tool.name);
|
|
293
|
+
if (raw !== void 0) return {
|
|
294
|
+
...tool,
|
|
295
|
+
inputSchema: raw
|
|
296
|
+
};
|
|
297
|
+
return tool;
|
|
298
|
+
})
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
//#endregion
|
|
305
|
+
Object.defineProperty(exports, 'McpServer', {
|
|
306
|
+
enumerable: true,
|
|
307
|
+
get: function () {
|
|
308
|
+
return _modelcontextprotocol_sdk_server_mcp_js.McpServer;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
exports.apiCall = apiCall;
|
|
312
|
+
exports.createMcpRouter = createMcpRouter;
|
|
313
|
+
exports.registerToolFromSchema = registerToolFromSchema;
|
|
314
|
+
Object.defineProperty(exports, 'z', {
|
|
315
|
+
enumerable: true,
|
|
316
|
+
get: function () {
|
|
317
|
+
return zod.z;
|
|
318
|
+
}
|
|
319
|
+
});
|