@ttoss/http-server-mcp 0.12.3 → 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.d.ts DELETED
@@ -1,244 +0,0 @@
1
- import * as koa from 'koa';
2
- import { Context } from 'koa';
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
- import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
- import { Router } from '@ttoss/http-server';
7
- export { z } from 'zod';
8
-
9
- /**
10
- * Options for a single `apiCall` request.
11
- */
12
- interface ApiCallOptions {
13
- /**
14
- * JSON-serialisable request body. Automatically serialised and sent with
15
- * `Content-Type: application/json`.
16
- */
17
- body?: unknown;
18
- /**
19
- * Additional or override headers for this specific request.
20
- * These are merged on top of any headers injected from the MCP request
21
- * context via `getApiHeaders`, allowing per-call overrides.
22
- */
23
- headers?: Record<string, string>;
24
- }
25
- /**
26
- * Generic HTTP helper for use inside MCP tool handlers.
27
- *
28
- * Accepts any full URL (third-party APIs, public APIs, etc.) or a path
29
- * relative to the `apiBaseUrl` configured in `createMcpRouter`.
30
- *
31
- * Headers configured via `getApiHeaders` in `createMcpRouter` are injected
32
- * automatically into every request, allowing transparent forwarding of auth
33
- * tokens, API keys, or any other header — without coupling this helper to a
34
- * specific authentication scheme. Per-call `options.headers` take precedence
35
- * over context-injected headers.
36
- *
37
- * @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
38
- * @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
39
- * @param options - Optional body and per-call header overrides
40
- * @returns Parsed JSON response body
41
- *
42
- * @example Bearer token forwarding (configured once in `createMcpRouter`)
43
- * ```typescript
44
- * import { apiCall, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
45
- *
46
- * // Tool handler – no manual auth wiring needed
47
- * mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
48
- * const data = await apiCall('GET', '/portfolios');
49
- * return { content: [{ type: 'text', text: JSON.stringify(data) }] };
50
- * });
51
- *
52
- * const mcpRouter = createMcpRouter(mcpServer, {
53
- * apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
54
- * // Forward the caller's Bearer token to every apiCall
55
- * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
56
- * });
57
- * ```
58
- *
59
- * @example x-api-key forwarding
60
- * ```typescript
61
- * const mcpRouter = createMcpRouter(mcpServer, {
62
- * apiBaseUrl: 'https://internal-service/api',
63
- * getApiHeaders: (ctx) => ({
64
- * 'x-api-key': ctx.headers['x-api-key'] as string,
65
- * }),
66
- * });
67
- * ```
68
- *
69
- * @example Third-party or public API (full URL, no context required)
70
- * ```typescript
71
- * const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
72
- * const created = await apiCall('POST', 'https://api.example.com/items', {
73
- * body: { name: 'widget' },
74
- * headers: { Authorization: 'Bearer fixed-service-token' },
75
- * });
76
- * ```
77
- */
78
- declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
79
- /**
80
- * Options for configuring the MCP router
81
- */
82
- interface McpRouterOptions {
83
- /**
84
- * The HTTP path where the MCP server will be mounted
85
- * @default '/mcp'
86
- */
87
- path?: string;
88
- /**
89
- * Optional session ID generator for stateful MCP servers.
90
- * When provided, a single shared transport is created and sessions are tracked.
91
- * When undefined (default), the server operates in stateless mode where each
92
- * HTTP request uses its own transport instance.
93
- */
94
- sessionIdGenerator?: () => string;
95
- /**
96
- * Base URL prepended to relative paths passed to `apiCall` (paths starting
97
- * with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
98
- * specifying a host.
99
- *
100
- * @example 'http://localhost:3000/api/v1'
101
- */
102
- apiBaseUrl?: string;
103
- /**
104
- * Called once per incoming MCP HTTP request. Return a plain object whose
105
- * key-value pairs will be merged into the headers of every `apiCall` made
106
- * within that request's tool handlers.
107
- *
108
- * Use this to forward any header from the MCP request — Bearer tokens, API
109
- * keys, tenant IDs, trace headers, etc. — without coupling tool handlers to
110
- * a specific authentication scheme.
111
- *
112
- * @example Forward a Bearer token
113
- * ```typescript
114
- * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' })
115
- * ```
116
- *
117
- * @example Forward an x-api-key header
118
- * ```typescript
119
- * getApiHeaders: (ctx) => ({ 'x-api-key': ctx.headers['x-api-key'] as string })
120
- * ```
121
- *
122
- * @example Inject a static service-to-service key
123
- * ```typescript
124
- * getApiHeaders: () => ({ 'x-internal-key': process.env.INTERNAL_API_KEY! })
125
- * ```
126
- */
127
- getApiHeaders?: (ctx: Context) => Record<string, string>;
128
- }
129
- /**
130
- * Creates a Koa router configured to handle MCP protocol requests
131
- *
132
- * @param server - The MCP server instance with registered tools and resources
133
- * @param options - Configuration options for the router
134
- * @returns A Koa Router instance configured for MCP
135
- *
136
- * @example
137
- * ```typescript
138
- * import { App, bodyParser } from '@ttoss/http-server';
139
- * import { createMcpRouter, McpServer, z } from '@ttoss/http-server-mcp';
140
- *
141
- * const mcpServer = new McpServer({
142
- * name: 'my-server',
143
- * version: '1.0.0',
144
- * });
145
- *
146
- * mcpServer.registerTool(
147
- * 'hello',
148
- * {
149
- * description: 'Say hello',
150
- * inputSchema: { name: z.string() },
151
- * },
152
- * async ({ name }) => ({
153
- * content: [{ type: 'text', text: `Hello, ${name}!` }],
154
- * })
155
- * );
156
- *
157
- * const app = new App();
158
- * app.use(bodyParser());
159
- *
160
- * const mcpRouter = createMcpRouter(mcpServer);
161
- * app.use(mcpRouter.routes());
162
- *
163
- * app.listen(3000);
164
- * ```
165
- */
166
- declare const createMcpRouter: (server: McpServer, options?: McpRouterOptions) => Router<koa.DefaultState, koa.DefaultContext>;
167
- /**
168
- * A plain JSON Schema object (draft-07 compatible) describing the shape of a
169
- * tool's input. Used with {@link registerToolFromSchema} as an alternative to
170
- * providing a Zod shape, enabling lossless round-trips for schemas that contain
171
- * features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
172
- */
173
- interface JsonObjectSchema {
174
- type: 'object';
175
- properties?: Record<string, unknown>;
176
- required?: string[];
177
- [key: string]: unknown;
178
- }
179
- /**
180
- * Parameters accepted by {@link registerToolFromSchema}.
181
- */
182
- interface RegisterToolFromSchemaParams {
183
- /** Unique tool name. */
184
- name: string;
185
- /** Human-readable description shown to the AI client. */
186
- description?: string;
187
- /**
188
- * Plain JSON Schema that describes the tool's input object.
189
- * This schema is forwarded verbatim over the MCP wire protocol, so any
190
- * JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
191
- * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
192
- */
193
- inputSchema?: JsonObjectSchema;
194
- /**
195
- * Tool handler invoked when the AI client calls the tool.
196
- * Receives the raw request arguments as a plain object (no Zod validation is
197
- * applied, so the shape matches whatever the client sends).
198
- */
199
- handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
200
- }
201
- /**
202
- * Registers a tool on an MCP server using a **plain JSON Schema** object for
203
- * `inputSchema` instead of a Zod shape.
204
- *
205
- * This is useful when tool definitions are shared between the MCP server and an
206
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
207
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
208
- * JSON-Schema→Zod conversion that would otherwise be required.
209
- *
210
- * Internally the helper:
211
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
212
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
213
- * `tools/call` handler correctly routes requests and delivers raw args to
214
- * your handler.
215
- * 2. Stores the original JSON Schema and patches the `tools/list` response
216
- * handler so clients receive the verbatim JSON Schema over the wire.
217
- *
218
- * @param server - The `McpServer` instance to register the tool on.
219
- * @param params - Tool configuration including name, description, inputSchema,
220
- * and handler.
221
- *
222
- * @example
223
- * ```typescript
224
- * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
225
- *
226
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
227
- *
228
- * registerToolFromSchema(server, {
229
- * name: 'get-project',
230
- * description: 'Get a project by ID',
231
- * inputSchema: {
232
- * type: 'object',
233
- * properties: { id: { type: 'string', description: 'Project public ID' } },
234
- * required: ['id'],
235
- * },
236
- * handler: async ({ id }) => ({
237
- * content: [{ type: 'text', text: `Project: ${id}` }],
238
- * }),
239
- * });
240
- * ```
241
- */
242
- declare const registerToolFromSchema: (server: McpServer, params: RegisterToolFromSchemaParams) => void;
243
-
244
- export { type ApiCallOptions, type JsonObjectSchema, type McpRouterOptions, type RegisterToolFromSchemaParams, apiCall, createMcpRouter, registerToolFromSchema };
package/dist/index.js DELETED
@@ -1,244 +0,0 @@
1
- /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- "use strict";
3
-
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __name = (target, value) => __defProp(target, "name", {
9
- value,
10
- configurable: true
11
- });
12
- var __export = (target, all) => {
13
- for (var name in all) __defProp(target, name, {
14
- get: all[name],
15
- enumerable: true
16
- });
17
- };
18
- var __copyProps = (to, from, except, desc) => {
19
- if (from && typeof from === "object" || typeof from === "function") {
20
- for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
21
- get: () => from[key],
22
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
23
- });
24
- }
25
- return to;
26
- };
27
- var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
28
- value: true
29
- }), mod);
30
-
31
- // src/index.ts
32
- var index_exports = {};
33
- __export(index_exports, {
34
- McpServer: () => import_mcp.McpServer,
35
- apiCall: () => apiCall,
36
- createMcpRouter: () => createMcpRouter,
37
- registerToolFromSchema: () => registerToolFromSchema,
38
- z: () => import_zod2.z
39
- });
40
- module.exports = __toCommonJS(index_exports);
41
- var import_node_async_hooks = require("async_hooks");
42
- var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
43
- var import_http_server = require("@ttoss/http-server");
44
- var import_zod = require("zod");
45
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
46
- var import_zod2 = require("zod");
47
- var requestContextStore = new import_node_async_hooks.AsyncLocalStorage();
48
- var apiCall = /* @__PURE__ */__name(async (method, url, options) => {
49
- const context = requestContextStore.getStore();
50
- let resolvedUrl = url;
51
- if (url.startsWith("/")) {
52
- if (!context?.apiBaseUrl) {
53
- 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.`);
54
- }
55
- resolvedUrl = `${context.apiBaseUrl.replace(/\/$/, "")}${url}`;
56
- }
57
- const hasBody = options?.body !== void 0;
58
- const headers = {
59
- ...(context !== void 0 ? context.apiHeaders : {}),
60
- ...(options?.headers ?? {})
61
- };
62
- const hasExplicitContentType = Object.keys(headers).some(headerName => {
63
- return headerName.toLowerCase() === "content-type";
64
- });
65
- if (hasBody && !hasExplicitContentType) {
66
- headers["Content-Type"] = "application/json";
67
- }
68
- const response = await fetch(resolvedUrl, {
69
- method,
70
- headers,
71
- body: hasBody ? JSON.stringify(options.body) : void 0
72
- });
73
- if (!response.ok) {
74
- const err = await response.json().catch(() => {
75
- return {
76
- error: response.statusText
77
- };
78
- });
79
- throw new Error(err.error || `HTTP ${response.status}`);
80
- }
81
- if (response.status === 204 || response.status === 205) {
82
- return void 0;
83
- }
84
- const contentType = response.headers.get("content-type");
85
- if (contentType?.includes("application/json")) {
86
- return response.json();
87
- }
88
- return response.text();
89
- }, "apiCall");
90
- var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
91
- const {
92
- path = "/mcp",
93
- sessionIdGenerator,
94
- apiBaseUrl,
95
- getApiHeaders
96
- } = options;
97
- const isStateful = sessionIdGenerator !== void 0;
98
- const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
99
- let sharedTransport;
100
- if (isStateful) {
101
- sharedTransport = new import_streamableHttp.StreamableHTTPServerTransport({
102
- sessionIdGenerator,
103
- enableJsonResponse: true
104
- });
105
- server.connect(sharedTransport);
106
- }
107
- let statelessQueue = Promise.resolve();
108
- const enqueueStateless = /* @__PURE__ */__name(work => {
109
- const result = statelessQueue.then(() => {
110
- return work();
111
- });
112
- statelessQueue = result.catch(() => {});
113
- return result;
114
- }, "enqueueStateless");
115
- const router = new import_http_server.Router();
116
- const handleWithContext = /* @__PURE__ */__name(async (ctx, body) => {
117
- const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
118
- const runRequest = /* @__PURE__ */__name(async transport => {
119
- await transport.handleRequest(ctx.req, ctx.res, body);
120
- ctx.respond = false;
121
- }, "runRequest");
122
- if (isStateful && sharedTransport) {
123
- if (needsContext) {
124
- await requestContextStore.run({
125
- apiBaseUrl,
126
- apiHeaders
127
- }, () => {
128
- return runRequest(sharedTransport);
129
- });
130
- } else {
131
- await runRequest(sharedTransport);
132
- }
133
- } else {
134
- await enqueueStateless(async () => {
135
- const transport = new import_streamableHttp.StreamableHTTPServerTransport({
136
- sessionIdGenerator: void 0,
137
- enableJsonResponse: true
138
- });
139
- try {
140
- await server.connect(transport);
141
- if (needsContext) {
142
- await requestContextStore.run({
143
- apiBaseUrl,
144
- apiHeaders
145
- }, () => {
146
- return runRequest(transport);
147
- });
148
- } else {
149
- await runRequest(transport);
150
- }
151
- } finally {
152
- await transport.close();
153
- }
154
- });
155
- }
156
- }, "handleWithContext");
157
- router.post(path, async ctx => {
158
- try {
159
- await handleWithContext(ctx, ctx.request.body);
160
- } catch (error) {
161
- if (!ctx.res.headersSent) {
162
- ctx.status = 500;
163
- ctx.body = {
164
- error: error instanceof Error ? error.message : "Internal server error"
165
- };
166
- }
167
- }
168
- });
169
- router.delete(path, async ctx => {
170
- try {
171
- await handleWithContext(ctx);
172
- } catch (error) {
173
- if (!ctx.res.headersSent) {
174
- ctx.status = 500;
175
- ctx.body = {
176
- error: error instanceof Error ? error.message : "Internal server error"
177
- };
178
- }
179
- }
180
- });
181
- return router;
182
- }, "createMcpRouter");
183
- var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
184
- var patchedServerSet = /* @__PURE__ */new WeakSet();
185
- var registerToolFromSchema = /* @__PURE__ */__name((server, params) => {
186
- const {
187
- name,
188
- description,
189
- inputSchema = {
190
- type: "object",
191
- properties: {}
192
- },
193
- handler
194
- } = params;
195
- if (!rawSchemaRegistryMap.has(server)) {
196
- rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
197
- }
198
- const registry = rawSchemaRegistryMap.get(server);
199
- registry.set(name, inputSchema);
200
- server.registerTool(name, {
201
- description,
202
- inputSchema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())
203
- }, async args => {
204
- return handler(args);
205
- });
206
- if (!patchedServerSet.has(server)) {
207
- patchedServerSet.add(server);
208
- const rawServer = server.server;
209
- const origHandler = rawServer?._requestHandlers?.get("tools/list");
210
- if (!origHandler && process.env.NODE_ENV !== "production") {
211
- console.warn("[registerToolFromSchema] Could not patch tools/list \u2014 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.");
212
- }
213
- if (origHandler) {
214
- rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
215
- const result = await origHandler(rawRequest, extra);
216
- const schemas = rawSchemaRegistryMap.get(server);
217
- if (!schemas) {
218
- return result;
219
- }
220
- return {
221
- ...result,
222
- tools: result.tools.map(tool => {
223
- const raw = schemas.get(tool.name);
224
- if (raw !== void 0) {
225
- return {
226
- ...tool,
227
- inputSchema: raw
228
- };
229
- }
230
- return tool;
231
- })
232
- };
233
- });
234
- }
235
- }
236
- }, "registerToolFromSchema");
237
- // Annotate the CommonJS export names for ESM import in node:
238
- 0 && (module.exports = {
239
- McpServer,
240
- apiCall,
241
- createMcpRouter,
242
- registerToolFromSchema,
243
- z
244
- });