@ttoss/http-server-mcp 0.12.4 → 0.13.0

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 CHANGED
@@ -127,6 +127,137 @@ const data = await apiCall('GET', 'https://partner.api.com/data', {
127
127
 
128
128
  `apiCall` throws with a clear message when called with a relative path and no `apiBaseUrl` is configured in the context.
129
129
 
130
+ ## Authentication
131
+
132
+ `createMcpRouter` supports OAuth 2.0 Bearer token authentication via the `auth` option. Every incoming MCP request must include a valid `Authorization: Bearer <token>` header — invalid or missing tokens receive a `401 Unauthorized` response.
133
+
134
+ ```mermaid
135
+ sequenceDiagram
136
+ participant Client
137
+ participant MCP Server
138
+ participant Verifier
139
+
140
+ Client->>MCP Server: POST /mcp + Authorization: Bearer &lt;token&gt;
141
+ MCP Server->>Verifier: verify(token)
142
+ alt valid token
143
+ Verifier-->>MCP Server: identity payload
144
+ MCP Server->>MCP Server: run tool (identity available via getIdentity())
145
+ MCP Server-->>Client: 200 OK
146
+ else invalid or missing token
147
+ Verifier-->>MCP Server: error
148
+ MCP Server-->>Client: 401 Unauthorized
149
+ end
150
+ ```
151
+
152
+ ### Amazon Cognito
153
+
154
+ Pass `cognitoUserPool` and the router creates a `CognitoJwtVerifier` (from `@ttoss/auth-core`) internally:
155
+
156
+ ```typescript
157
+ import { createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
158
+
159
+ const mcpRouter = createMcpRouter(mcpServer, {
160
+ auth: {
161
+ cognitoUserPool: {
162
+ userPoolId: process.env.COGNITO_USER_POOL_ID!,
163
+ clientId: process.env.COGNITO_CLIENT_ID!,
164
+ tokenUse: 'access', // default
165
+ },
166
+ },
167
+ });
168
+ ```
169
+
170
+ ### Custom verifier
171
+
172
+ Pass an async `verifyToken` function for any other provider (Auth0, Keycloak, etc.):
173
+
174
+ ```typescript
175
+ import { createMcpRouter } from '@ttoss/http-server-mcp';
176
+ import { jwtVerify, createRemoteJWKSet } from 'jose';
177
+
178
+ const JWKS = createRemoteJWKSet(
179
+ new URL('https://your-auth-server/.well-known/jwks.json')
180
+ );
181
+
182
+ const mcpRouter = createMcpRouter(mcpServer, {
183
+ auth: {
184
+ verifyToken: async (token) => {
185
+ const { payload } = await jwtVerify(token, JWKS);
186
+ return payload;
187
+ },
188
+ },
189
+ });
190
+ ```
191
+
192
+ ### Accessing the verified identity
193
+
194
+ Inside any tool handler, call `getIdentity()` to retrieve the verified JWT payload:
195
+
196
+ ```typescript
197
+ import { getIdentity, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
198
+
199
+ mcpServer.registerTool(
200
+ 'get-profile',
201
+ { description: 'Return the caller's profile', inputSchema: {} },
202
+ async () => {
203
+ const identity = getIdentity() as { sub: string; email: string };
204
+ return {
205
+ content: [{ type: 'text', text: `Hello, ${identity.email}` }],
206
+ };
207
+ }
208
+ );
209
+ ```
210
+
211
+ ### Scope enforcement
212
+
213
+ Scopes can be enforced at two levels.
214
+
215
+ **Router-level** — gate the entire MCP endpoint. Any token missing a required scope receives a `403 Forbidden` before any tool runs:
216
+
217
+ ```typescript
218
+ createMcpRouter(mcpServer, {
219
+ auth: {
220
+ cognitoUserPool: { userPoolId: '...', clientId: '...' },
221
+ requiredScopes: ['mcp:access'],
222
+ },
223
+ });
224
+ ```
225
+
226
+ **Per-tool** — use `checkScopes()` inside individual handlers for fine-grained control. It throws an error that the MCP SDK returns as a tool error to the client:
227
+
228
+ ```typescript
229
+ import { checkScopes, getIdentity } from '@ttoss/http-server-mcp';
230
+
231
+ mcpServer.registerTool(
232
+ 'delete-user',
233
+ { description: 'Delete a user', inputSchema: { userId: z.string() } },
234
+ async ({ userId }) => {
235
+ checkScopes(['admin', 'write:users']); // throws if either scope is missing
236
+
237
+ const identity = getIdentity() as { sub: string };
238
+ // proceed with deletion...
239
+ return { content: [{ type: 'text', text: `Deleted ${userId}` }] };
240
+ }
241
+ );
242
+ ```
243
+
244
+ Cognito encodes scopes as a space-separated string in `payload.scope` (e.g. `"openid mcp:access admin"`).
245
+
246
+ ### OAuth Protected Resource Metadata
247
+
248
+ For MCP clients that support OAuth auto-discovery, add `resourceServerUrl` and `authorizationServerUrl` to expose the `/.well-known/oauth-protected-resource` endpoint (RFC 9728):
249
+
250
+ ```typescript
251
+ createMcpRouter(mcpServer, {
252
+ auth: {
253
+ cognitoUserPool: { userPoolId: '...', clientId: '...' },
254
+ resourceServerUrl: 'https://mcp.example.com',
255
+ authorizationServerUrl:
256
+ 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx',
257
+ },
258
+ });
259
+ ```
260
+
130
261
  ## API Reference
131
262
 
132
263
  ### `createMcpRouter(server, options?)`
@@ -135,14 +266,19 @@ Creates a Koa router configured to handle MCP protocol requests.
135
266
 
136
267
  **Parameters:**
137
268
 
138
- - `server` (`McpServer`) - MCP server instance with registered tools and resources
139
- - `options` (`McpRouterOptions`) - Optional configuration
140
- - `path` (`string`) - HTTP path for MCP endpoint (default: `'/mcp'`)
141
- - `sessionIdGenerator` (`() => string`) - Session ID generator for stateful servers (default: `undefined` for stateless)
142
- - `apiBaseUrl` (`string`) - Base URL prepended to relative paths in `apiCall`
143
- - `getApiHeaders` (`(ctx: Context) => Record<string, string>`) - Return headers to inject into every `apiCall` for this request (auth tokens, API keys, trace headers, etc.)
269
+ - `server` (`McpServer`) MCP server instance with registered tools and resources
270
+ - `options` (`McpRouterOptions`) Optional configuration
271
+ - `path` (`string`) HTTP path for MCP endpoint (default: `'/mcp'`)
272
+ - `sessionIdGenerator` (`() => string`) Session ID generator for stateful servers (default: `undefined` for stateless)
273
+ - `apiBaseUrl` (`string`) Base URL prepended to relative paths in `apiCall`
274
+ - `getApiHeaders` (`(ctx: Context) => Record<string, string>`) Return headers to inject into every `apiCall` for this request
275
+ - `auth` (`McpAuthOptions`) — OAuth/JWT authentication; see [Authentication](#authentication)
276
+ - `auth.cognitoUserPool` — Cognito user pool config (`userPoolId`, `clientId`, `tokenUse`)
277
+ - `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
278
+ - `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
279
+ - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`
144
280
 
145
- **Returns:** `Router` - Koa router instance
281
+ **Returns:** `Router` Koa router instance
146
282
 
147
283
  ### `apiCall(method, url, options?)`
148
284
 
@@ -150,12 +286,26 @@ Generic HTTP helper for use inside MCP tool handlers.
150
286
 
151
287
  **Parameters:**
152
288
 
153
- - `method` (`string`) - HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'DELETE'`, …)
154
- - `url` (`string`) - Full URL **or** a path starting with `/` (prepended with `apiBaseUrl`)
155
- - `options.body` (`unknown`, optional) - Request body, serialised as JSON
156
- - `options.headers` (`Record<string, string>`, optional) - Per-call header overrides; merged on top of context-injected headers
289
+ - `method` (`string`) HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'DELETE'`, …)
290
+ - `url` (`string`) Full URL **or** a path starting with `/` (prepended with `apiBaseUrl`)
291
+ - `options.body` (`unknown`, optional) Request body, serialised as JSON
292
+ - `options.headers` (`Record<string, string>`, optional) Per-call header overrides; merged on top of context-injected headers
293
+
294
+ **Returns:** `Promise<unknown>` — Parsed JSON response body
295
+
296
+ ### `getIdentity()`
297
+
298
+ Returns the verified JWT payload for the current MCP request. Only available inside a tool handler when `auth` is configured. Returns `undefined` when called outside an authenticated context.
299
+
300
+ **Returns:** `unknown` — Verified token payload (cast to your expected shape)
301
+
302
+ ### `checkScopes(required)`
303
+
304
+ Asserts that the current request token contains all required scopes. Throws `Error: Insufficient scopes. Required: …` if any scope is missing — the MCP SDK catches this and returns a tool error to the client.
305
+
306
+ **Parameters:**
157
307
 
158
- **Returns:** `Promise<unknown>` - Parsed JSON response body
308
+ - `required` (`string[]`) Scope strings that must all be present in `payload.scope`
159
309
 
160
310
  ### `registerToolFromSchema(server, params)`
161
311
 
@@ -165,11 +315,11 @@ Use this when tool definitions are shared between the MCP server and an AI SDK a
165
315
 
166
316
  **Parameters:**
167
317
 
168
- - `server` (`McpServer`) - The MCP server instance
169
- - `params.name` (`string`) - Unique tool name
170
- - `params.description` (`string`, optional) - Human-readable description
171
- - `params.inputSchema` (`JsonObjectSchema`, optional) - Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
172
- - `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) - Tool handler receiving the raw request arguments
318
+ - `server` (`McpServer`) The MCP server instance
319
+ - `params.name` (`string`) Unique tool name
320
+ - `params.description` (`string`, optional) Human-readable description
321
+ - `params.inputSchema` (`JsonObjectSchema`, optional) Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
322
+ - `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) Tool handler receiving the raw request arguments
173
323
 
174
324
  **Returns:** `void`
175
325
 
package/dist/index.cjs ADDED
@@ -0,0 +1,401 @@
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_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
8
+ let _ttoss_http_server = require("@ttoss/http-server");
9
+ let zod = require("zod");
10
+ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
11
+
12
+ //#region src/index.ts
13
+ const requestContextStore = new node_async_hooks.AsyncLocalStorage();
14
+ /**
15
+ * Generic HTTP helper for use inside MCP tool handlers.
16
+ *
17
+ * Accepts any full URL (third-party APIs, public APIs, etc.) or a path
18
+ * relative to the `apiBaseUrl` configured in `createMcpRouter`.
19
+ *
20
+ * Headers configured via `getApiHeaders` in `createMcpRouter` are injected
21
+ * automatically into every request, allowing transparent forwarding of auth
22
+ * tokens, API keys, or any other header — without coupling this helper to a
23
+ * specific authentication scheme. Per-call `options.headers` take precedence
24
+ * over context-injected headers.
25
+ *
26
+ * @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
27
+ * @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
28
+ * @param options - Optional body and per-call header overrides
29
+ * @returns Parsed JSON response body
30
+ *
31
+ * @example Bearer token forwarding (configured once in `createMcpRouter`)
32
+ * ```typescript
33
+ * import { apiCall, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
34
+ *
35
+ * // Tool handler – no manual auth wiring needed
36
+ * mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
37
+ * const data = await apiCall('GET', '/portfolios');
38
+ * return { content: [{ type: 'text', text: JSON.stringify(data) }] };
39
+ * });
40
+ *
41
+ * const mcpRouter = createMcpRouter(mcpServer, {
42
+ * apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
43
+ * // Forward the caller's Bearer token to every apiCall
44
+ * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
45
+ * });
46
+ * ```
47
+ *
48
+ * @example x-api-key forwarding
49
+ * ```typescript
50
+ * const mcpRouter = createMcpRouter(mcpServer, {
51
+ * apiBaseUrl: 'https://internal-service/api',
52
+ * getApiHeaders: (ctx) => ({
53
+ * 'x-api-key': ctx.headers['x-api-key'] as string,
54
+ * }),
55
+ * });
56
+ * ```
57
+ *
58
+ * @example Third-party or public API (full URL, no context required)
59
+ * ```typescript
60
+ * const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
61
+ * const created = await apiCall('POST', 'https://api.example.com/items', {
62
+ * body: { name: 'widget' },
63
+ * headers: { Authorization: 'Bearer fixed-service-token' },
64
+ * });
65
+ * ```
66
+ */
67
+ const apiCall = async (method, url, options) => {
68
+ const context = requestContextStore.getStore();
69
+ let resolvedUrl = url;
70
+ if (url.startsWith("/")) {
71
+ 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.`);
72
+ resolvedUrl = `${context.apiBaseUrl.replace(/\/$/, "")}${url}`;
73
+ }
74
+ const hasBody = options?.body !== void 0;
75
+ const headers = {
76
+ ...(context !== void 0 ? context.apiHeaders : {}),
77
+ ...(options?.headers ?? {})
78
+ };
79
+ const hasExplicitContentType = Object.keys(headers).some(headerName => {
80
+ return headerName.toLowerCase() === "content-type";
81
+ });
82
+ if (hasBody && !hasExplicitContentType) headers["Content-Type"] = "application/json";
83
+ const response = await fetch(resolvedUrl, {
84
+ method,
85
+ headers,
86
+ body: hasBody ? JSON.stringify(options.body) : void 0
87
+ });
88
+ if (!response.ok) {
89
+ const err = await response.json().catch(() => {
90
+ return {
91
+ error: response.statusText
92
+ };
93
+ });
94
+ throw new Error(err.error || `HTTP ${response.status}`);
95
+ }
96
+ if (response.status === 204 || response.status === 205) return;
97
+ if (response.headers.get("content-type")?.includes("application/json")) return response.json();
98
+ return response.text();
99
+ };
100
+ /**
101
+ * Returns the verified JWT payload for the current MCP request.
102
+ * Only available inside a tool handler when `auth` is configured on the router.
103
+ */
104
+ const getIdentity = () => {
105
+ return requestContextStore.getStore()?.identity;
106
+ };
107
+ /**
108
+ * Asserts that the current request's token contains all required scopes.
109
+ * Throws if any scope is missing — the MCP SDK catches this and returns a
110
+ * tool error to the client. Use inside tool handlers for per-tool authorization.
111
+ *
112
+ * Cognito tokens carry scopes as a space-separated string in `payload.scope`.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * server.tool('delete-user', schema, async (args) => {
117
+ * checkScopes(['admin', 'write:users']);
118
+ * // proceed only if caller has both scopes
119
+ * });
120
+ * ```
121
+ */
122
+ const checkScopes = required => {
123
+ const tokenScopes = (getIdentity()?.scope ?? "").split(" ");
124
+ if (required.filter(s => {
125
+ return !tokenScopes.includes(s);
126
+ }).length > 0) throw new Error(`Insufficient scopes. Required: ${required.join(", ")}`);
127
+ };
128
+ const buildTokenVerifier = auth => {
129
+ if (auth.cognitoUserPool) {
130
+ const v = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
131
+ tokenUse: "access",
132
+ ...auth.cognitoUserPool
133
+ });
134
+ return t => {
135
+ return v.verify(t);
136
+ };
137
+ }
138
+ if (auth.verifyToken) return auth.verifyToken;
139
+ throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
140
+ };
141
+ const registerAuthRoutes = (router, path, auth, tokenVerifier) => {
142
+ router.use(path, async (ctx, next) => {
143
+ const authHeader = ctx.headers.authorization;
144
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
145
+ let identity;
146
+ try {
147
+ identity = await tokenVerifier(token);
148
+ } catch {
149
+ ctx.status = 401;
150
+ ctx.set("WWW-Authenticate", "Bearer");
151
+ ctx.body = "Unauthorized";
152
+ return;
153
+ }
154
+ if (auth.requiredScopes?.length) {
155
+ const tokenScopes = (identity?.scope ?? "").split(" ");
156
+ if (auth.requiredScopes.filter(s => {
157
+ return !tokenScopes.includes(s);
158
+ }).length > 0) {
159
+ ctx.status = 403;
160
+ ctx.body = "Forbidden";
161
+ return;
162
+ }
163
+ }
164
+ ctx.state.identity = identity;
165
+ await next();
166
+ });
167
+ if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
168
+ ctx.body = {
169
+ resource: auth.resourceServerUrl,
170
+ authorization_servers: [auth.authorizationServerUrl]
171
+ };
172
+ });
173
+ };
174
+ /**
175
+ * Creates a Koa router configured to handle MCP protocol requests
176
+ *
177
+ * @param server - The MCP server instance with registered tools and resources
178
+ * @param options - Configuration options for the router
179
+ * @returns A Koa Router instance configured for MCP
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * import { App, bodyParser } from '@ttoss/http-server';
184
+ * import { createMcpRouter, McpServer, z } from '@ttoss/http-server-mcp';
185
+ *
186
+ * const mcpServer = new McpServer({
187
+ * name: 'my-server',
188
+ * version: '1.0.0',
189
+ * });
190
+ *
191
+ * mcpServer.registerTool(
192
+ * 'hello',
193
+ * {
194
+ * description: 'Say hello',
195
+ * inputSchema: { name: z.string() },
196
+ * },
197
+ * async ({ name }) => ({
198
+ * content: [{ type: 'text', text: `Hello, ${name}!` }],
199
+ * })
200
+ * );
201
+ *
202
+ * const app = new App();
203
+ * app.use(bodyParser());
204
+ *
205
+ * const mcpRouter = createMcpRouter(mcpServer);
206
+ * app.use(mcpRouter.routes());
207
+ *
208
+ * app.listen(3000);
209
+ * ```
210
+ */
211
+ const createMcpRouter = (server, options = {}) => {
212
+ const {
213
+ path = "/mcp",
214
+ sessionIdGenerator,
215
+ apiBaseUrl,
216
+ getApiHeaders,
217
+ auth
218
+ } = options;
219
+ const isStateful = sessionIdGenerator !== void 0;
220
+ const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
221
+ let sharedTransport;
222
+ if (isStateful) {
223
+ sharedTransport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
224
+ sessionIdGenerator,
225
+ enableJsonResponse: true
226
+ });
227
+ server.connect(sharedTransport);
228
+ }
229
+ let statelessQueue = Promise.resolve();
230
+ const enqueueStateless = work => {
231
+ const result = statelessQueue.then(() => {
232
+ return work();
233
+ });
234
+ statelessQueue = result.catch(() => {});
235
+ return result;
236
+ };
237
+ const router = new _ttoss_http_server.Router();
238
+ if (auth) registerAuthRoutes(router, path, auth, buildTokenVerifier(auth));
239
+ const handleWithContext = async (ctx, body) => {
240
+ const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
241
+ const identity = ctx.state.identity;
242
+ const runRequest = async transport => {
243
+ await transport.handleRequest(ctx.req, ctx.res, body);
244
+ ctx.respond = false;
245
+ };
246
+ if (isStateful && sharedTransport) {
247
+ if (needsContext) await requestContextStore.run({
248
+ apiBaseUrl,
249
+ apiHeaders,
250
+ identity
251
+ }, () => {
252
+ return runRequest(sharedTransport);
253
+ });else await runRequest(sharedTransport);
254
+ } else await enqueueStateless(async () => {
255
+ const transport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
256
+ sessionIdGenerator: void 0,
257
+ enableJsonResponse: true
258
+ });
259
+ try {
260
+ await server.connect(transport);
261
+ if (needsContext) await requestContextStore.run({
262
+ apiBaseUrl,
263
+ apiHeaders,
264
+ identity
265
+ }, () => {
266
+ return runRequest(transport);
267
+ });else await runRequest(transport);
268
+ } finally {
269
+ await transport.close();
270
+ }
271
+ });
272
+ };
273
+ router.post(path, async ctx => {
274
+ try {
275
+ await handleWithContext(ctx, ctx.request.body);
276
+ } catch (error) {
277
+ if (!ctx.res.headersSent) {
278
+ ctx.status = 500;
279
+ ctx.body = {
280
+ error: error instanceof Error ? error.message : "Internal server error"
281
+ };
282
+ }
283
+ }
284
+ });
285
+ router.delete(path, async ctx => {
286
+ try {
287
+ await handleWithContext(ctx);
288
+ } catch (error) {
289
+ if (!ctx.res.headersSent) {
290
+ ctx.status = 500;
291
+ ctx.body = {
292
+ error: error instanceof Error ? error.message : "Internal server error"
293
+ };
294
+ }
295
+ }
296
+ });
297
+ return router;
298
+ };
299
+ const rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
300
+ const patchedServerSet = /* @__PURE__ */new WeakSet();
301
+ /**
302
+ * Registers a tool on an MCP server using a **plain JSON Schema** object for
303
+ * `inputSchema` instead of a Zod shape.
304
+ *
305
+ * This is useful when tool definitions are shared between the MCP server and an
306
+ * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
307
+ * plain JSON Schema at runtime. Using this helper eliminates the lossy
308
+ * JSON-Schema→Zod conversion that would otherwise be required.
309
+ *
310
+ * Internally the helper:
311
+ * 1. Registers the tool via the standard `McpServer.registerTool` API using a
312
+ * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
313
+ * `tools/call` handler correctly routes requests and delivers raw args to
314
+ * your handler.
315
+ * 2. Stores the original JSON Schema and patches the `tools/list` response
316
+ * handler so clients receive the verbatim JSON Schema over the wire.
317
+ *
318
+ * @param server - The `McpServer` instance to register the tool on.
319
+ * @param params - Tool configuration including name, description, inputSchema,
320
+ * and handler.
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
325
+ *
326
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
327
+ *
328
+ * registerToolFromSchema(server, {
329
+ * name: 'get-project',
330
+ * description: 'Get a project by ID',
331
+ * inputSchema: {
332
+ * type: 'object',
333
+ * properties: { id: { type: 'string', description: 'Project public ID' } },
334
+ * required: ['id'],
335
+ * },
336
+ * handler: async ({ id }) => ({
337
+ * content: [{ type: 'text', text: `Project: ${id}` }],
338
+ * }),
339
+ * });
340
+ * ```
341
+ */
342
+ const registerToolFromSchema = (server, params) => {
343
+ const {
344
+ name,
345
+ description,
346
+ inputSchema = {
347
+ type: "object",
348
+ properties: {}
349
+ },
350
+ handler
351
+ } = params;
352
+ if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
353
+ rawSchemaRegistryMap.get(server).set(name, inputSchema);
354
+ server.registerTool(name, {
355
+ description,
356
+ inputSchema: zod.z.record(zod.z.string(), zod.z.unknown())
357
+ }, async args => {
358
+ return handler(args);
359
+ });
360
+ if (!patchedServerSet.has(server)) {
361
+ patchedServerSet.add(server);
362
+ const rawServer = server.server;
363
+ const origHandler = rawServer?._requestHandlers?.get("tools/list");
364
+ 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.");
365
+ if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
366
+ const result = await origHandler(rawRequest, extra);
367
+ const schemas = rawSchemaRegistryMap.get(server);
368
+ if (!schemas) return result;
369
+ return {
370
+ ...result,
371
+ tools: result.tools.map(tool => {
372
+ const raw = schemas.get(tool.name);
373
+ if (raw !== void 0) return {
374
+ ...tool,
375
+ inputSchema: raw
376
+ };
377
+ return tool;
378
+ })
379
+ };
380
+ });
381
+ }
382
+ };
383
+
384
+ //#endregion
385
+ Object.defineProperty(exports, 'McpServer', {
386
+ enumerable: true,
387
+ get: function () {
388
+ return _modelcontextprotocol_sdk_server_mcp_js.McpServer;
389
+ }
390
+ });
391
+ exports.apiCall = apiCall;
392
+ exports.checkScopes = checkScopes;
393
+ exports.createMcpRouter = createMcpRouter;
394
+ exports.getIdentity = getIdentity;
395
+ exports.registerToolFromSchema = registerToolFromSchema;
396
+ Object.defineProperty(exports, 'z', {
397
+ enumerable: true,
398
+ get: function () {
399
+ return zod.z;
400
+ }
401
+ });