@ttoss/http-server-mcp 0.5.7 → 0.6.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
@@ -57,6 +57,84 @@ app.listen(3000, () => {
57
57
  });
58
58
  ```
59
59
 
60
+ ## `apiCall` — Generic HTTP Helper
61
+
62
+ `apiCall` is a generic HTTP helper for use inside MCP tool handlers. It works with any URL — your own REST API, third-party APIs, public APIs, or services using `x-api-key` or any other header scheme.
63
+
64
+ Use `getApiHeaders` in `createMcpRouter` to configure which headers from the incoming MCP request are automatically forwarded to every `apiCall`. Tool handlers stay clean and auth-agnostic.
65
+
66
+ ### Bearer token forwarding
67
+
68
+ ```typescript
69
+ import {
70
+ apiCall,
71
+ createMcpRouter,
72
+ Server as McpServer,
73
+ } from '@ttoss/http-server-mcp';
74
+
75
+ const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
76
+
77
+ mcpServer.registerTool(
78
+ 'list-portfolios',
79
+ { description: 'List all portfolios', inputSchema: {} },
80
+ async () => {
81
+ // Bearer token is forwarded automatically — no manual wiring
82
+ const data = await apiCall('GET', '/portfolios');
83
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
84
+ }
85
+ );
86
+
87
+ const mcpRouter = createMcpRouter(mcpServer, {
88
+ apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
89
+ // Extract the caller's Bearer token and inject it into every apiCall
90
+ getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
91
+ });
92
+ ```
93
+
94
+ ### x-api-key forwarding
95
+
96
+ ```typescript
97
+ const mcpRouter = createMcpRouter(mcpServer, {
98
+ apiBaseUrl: 'https://internal-service/api',
99
+ getApiHeaders: (ctx) => ({
100
+ 'x-api-key': ctx.headers['x-api-key'] as string,
101
+ }),
102
+ });
103
+ ```
104
+
105
+ ### Third-party or public APIs (full URL, no context required)
106
+
107
+ ```typescript
108
+ mcpServer.registerTool(
109
+ 'get-rates',
110
+ { description: 'Currency rates', inputSchema: {} },
111
+ async () => {
112
+ // Full URL — works entirely outside any context
113
+ const rates = await apiCall('GET', 'https://api.exchangerate.host/latest');
114
+ return { content: [{ type: 'text', text: JSON.stringify(rates) }] };
115
+ }
116
+ );
117
+ ```
118
+
119
+ ### POST with a body
120
+
121
+ ```typescript
122
+ const result = await apiCall('POST', '/portfolios', {
123
+ body: { name: 'Growth Fund' },
124
+ });
125
+ ```
126
+
127
+ ### Per-call header override
128
+
129
+ ```typescript
130
+ // Context-injected headers are merged; per-call headers take precedence
131
+ const data = await apiCall('GET', 'https://partner.api.com/data', {
132
+ headers: { Authorization: 'Bearer fixed-service-token' },
133
+ });
134
+ ```
135
+
136
+ `apiCall` throws with a clear message when called with a relative path and no `apiBaseUrl` is configured in the context.
137
+
60
138
  ## API Reference
61
139
 
62
140
  ### `createMcpRouter(server, options?)`
@@ -69,9 +147,24 @@ Creates a Koa router configured to handle MCP protocol requests.
69
147
  - `options` (`McpRouterOptions`) - Optional configuration
70
148
  - `path` (`string`) - HTTP path for MCP endpoint (default: `'/mcp'`)
71
149
  - `sessionIdGenerator` (`() => string`) - Session ID generator for stateful servers (default: `undefined` for stateless)
150
+ - `apiBaseUrl` (`string`) - Base URL prepended to relative paths in `apiCall`
151
+ - `getApiHeaders` (`(ctx: Context) => Record<string, string>`) - Return headers to inject into every `apiCall` for this request (auth tokens, API keys, trace headers, etc.)
72
152
 
73
153
  **Returns:** `Router` - Koa router instance
74
154
 
155
+ ### `apiCall(method, url, options?)`
156
+
157
+ Generic HTTP helper for use inside MCP tool handlers.
158
+
159
+ **Parameters:**
160
+
161
+ - `method` (`string`) - HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'DELETE'`, …)
162
+ - `url` (`string`) - Full URL **or** a path starting with `/` (prepended with `apiBaseUrl`)
163
+ - `options.body` (`unknown`, optional) - Request body, serialised as JSON
164
+ - `options.headers` (`Record<string, string>`, optional) - Per-call header overrides; merged on top of context-injected headers
165
+
166
+ **Returns:** `Promise<unknown>` - Parsed JSON response body
167
+
75
168
  ## Examples
76
169
 
77
170
  ### Basic Tool
@@ -202,6 +295,16 @@ This package implements the [Model Context Protocol](https://spec.modelcontextpr
202
295
  - `POST /mcp` - Send JSON-RPC requests/notifications
203
296
  - `DELETE /mcp` - Terminate session (optional)
204
297
 
298
+ **Client requirements (per MCP spec):**
299
+
300
+ - `Content-Type: application/json`
301
+ - `Accept: application/json, text/event-stream`
302
+
303
+ **Stateless vs stateful mode:**
304
+
305
+ - **Stateless** (default, `sessionIdGenerator: undefined`) — a fresh transport is created per HTTP request. No session tracking. Suitable for serverless environments and simple integrations.
306
+ - **Stateful** (`sessionIdGenerator` provided) — a single shared transport handles all requests and tracks sessions by ID.
307
+
205
308
  ## Related Packages
206
309
 
207
310
  - [@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server) - HTTP server foundation
package/dist/esm/index.js CHANGED
@@ -6,25 +6,124 @@ var __name = (target, value) => __defProp(target, "name", {
6
6
  });
7
7
 
8
8
  // src/index.ts
9
+ import { AsyncLocalStorage } from "async_hooks";
9
10
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
11
  import { Router } from "@ttoss/http-server";
11
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
13
  import { z } from "zod";
14
+ var requestContextStore = new AsyncLocalStorage();
15
+ var apiCall = /* @__PURE__ */__name(async (method, url, options) => {
16
+ const context = requestContextStore.getStore();
17
+ let resolvedUrl = url;
18
+ if (url.startsWith("/")) {
19
+ if (!context?.apiBaseUrl) {
20
+ 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.`);
21
+ }
22
+ resolvedUrl = `${context.apiBaseUrl.replace(/\/$/, "")}${url}`;
23
+ }
24
+ const hasBody = options?.body !== void 0;
25
+ const headers = {
26
+ ...(context !== void 0 ? context.apiHeaders : {}),
27
+ ...(options?.headers ?? {})
28
+ };
29
+ const hasExplicitContentType = Object.keys(headers).some(headerName => {
30
+ return headerName.toLowerCase() === "content-type";
31
+ });
32
+ if (hasBody && !hasExplicitContentType) {
33
+ headers["Content-Type"] = "application/json";
34
+ }
35
+ const response = await fetch(resolvedUrl, {
36
+ method,
37
+ headers,
38
+ body: hasBody ? JSON.stringify(options.body) : void 0
39
+ });
40
+ if (!response.ok) {
41
+ const err = await response.json().catch(() => {
42
+ return {
43
+ error: response.statusText
44
+ };
45
+ });
46
+ throw new Error(err.error || `HTTP ${response.status}`);
47
+ }
48
+ if (response.status === 204 || response.status === 205) {
49
+ return void 0;
50
+ }
51
+ const contentType = response.headers.get("content-type");
52
+ if (contentType?.includes("application/json")) {
53
+ return response.json();
54
+ }
55
+ return response.text();
56
+ }, "apiCall");
13
57
  var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
14
58
  const {
15
59
  path = "/mcp",
16
- sessionIdGenerator
17
- } = options;
18
- const transport = new StreamableHTTPServerTransport({
19
60
  sessionIdGenerator,
20
- enableJsonResponse: true
21
- });
22
- server.connect(transport);
61
+ apiBaseUrl,
62
+ getApiHeaders
63
+ } = options;
64
+ const isStateful = sessionIdGenerator !== void 0;
65
+ const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
66
+ let sharedTransport;
67
+ if (isStateful) {
68
+ sharedTransport = new StreamableHTTPServerTransport({
69
+ sessionIdGenerator,
70
+ enableJsonResponse: true
71
+ });
72
+ server.connect(sharedTransport);
73
+ }
74
+ let statelessQueue = Promise.resolve();
75
+ const enqueueStateless = /* @__PURE__ */__name(work => {
76
+ const result = statelessQueue.then(() => {
77
+ return work();
78
+ });
79
+ statelessQueue = result.catch(() => {});
80
+ return result;
81
+ }, "enqueueStateless");
23
82
  const router = new Router();
83
+ const handleWithContext = /* @__PURE__ */__name(async (ctx, body) => {
84
+ const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
85
+ const runRequest = /* @__PURE__ */__name(async transport => {
86
+ await transport.handleRequest(ctx.req, ctx.res, body);
87
+ ctx.respond = false;
88
+ }, "runRequest");
89
+ if (isStateful && sharedTransport) {
90
+ if (needsContext) {
91
+ await requestContextStore.run({
92
+ apiBaseUrl,
93
+ apiHeaders
94
+ }, () => {
95
+ return runRequest(sharedTransport);
96
+ });
97
+ } else {
98
+ await runRequest(sharedTransport);
99
+ }
100
+ } else {
101
+ await enqueueStateless(async () => {
102
+ const transport = new StreamableHTTPServerTransport({
103
+ sessionIdGenerator: void 0,
104
+ enableJsonResponse: true
105
+ });
106
+ try {
107
+ await server.connect(transport);
108
+ if (needsContext) {
109
+ await requestContextStore.run({
110
+ apiBaseUrl,
111
+ apiHeaders
112
+ }, () => {
113
+ return runRequest(transport);
114
+ });
115
+ } else {
116
+ await runRequest(transport);
117
+ }
118
+ } finally {
119
+ await transport.close();
120
+ }
121
+ });
122
+ }
123
+ }, "handleWithContext");
24
124
  router.post(path, async ctx => {
25
125
  try {
26
- await transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
27
- ctx.respond = false;
126
+ await handleWithContext(ctx, ctx.request.body);
28
127
  } catch (error) {
29
128
  if (!ctx.res.headersSent) {
30
129
  ctx.status = 500;
@@ -36,8 +135,7 @@ var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
36
135
  });
37
136
  router.delete(path, async ctx => {
38
137
  try {
39
- await transport.handleRequest(ctx.req, ctx.res, void 0);
40
- ctx.respond = false;
138
+ await handleWithContext(ctx);
41
139
  } catch (error) {
42
140
  if (!ctx.res.headersSent) {
43
141
  ctx.status = 500;
@@ -49,4 +147,4 @@ var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
49
147
  });
50
148
  return router;
51
149
  }, "createMcpRouter");
52
- export { McpServer as Server, createMcpRouter, z };
150
+ export { McpServer as Server, apiCall, createMcpRouter, z };
package/dist/index.d.cts CHANGED
@@ -1,9 +1,80 @@
1
1
  import * as _koa_router from '@koa/router';
2
2
  import * as koa from 'koa';
3
+ import { Context } from 'koa';
3
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
5
  export { McpServer, McpServer as Server } from '@modelcontextprotocol/sdk/server/mcp.js';
5
6
  export { z } from 'zod';
6
7
 
8
+ /**
9
+ * Options for a single `apiCall` request.
10
+ */
11
+ interface ApiCallOptions {
12
+ /**
13
+ * JSON-serialisable request body. Automatically serialised and sent with
14
+ * `Content-Type: application/json`.
15
+ */
16
+ body?: unknown;
17
+ /**
18
+ * Additional or override headers for this specific request.
19
+ * These are merged on top of any headers injected from the MCP request
20
+ * context via `getApiHeaders`, allowing per-call overrides.
21
+ */
22
+ headers?: Record<string, string>;
23
+ }
24
+ /**
25
+ * Generic HTTP helper for use inside MCP tool handlers.
26
+ *
27
+ * Accepts any full URL (third-party APIs, public APIs, etc.) or a path
28
+ * relative to the `apiBaseUrl` configured in `createMcpRouter`.
29
+ *
30
+ * Headers configured via `getApiHeaders` in `createMcpRouter` are injected
31
+ * automatically into every request, allowing transparent forwarding of auth
32
+ * tokens, API keys, or any other header — without coupling this helper to a
33
+ * specific authentication scheme. Per-call `options.headers` take precedence
34
+ * over context-injected headers.
35
+ *
36
+ * @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
37
+ * @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
38
+ * @param options - Optional body and per-call header overrides
39
+ * @returns Parsed JSON response body
40
+ *
41
+ * @example Bearer token forwarding (configured once in `createMcpRouter`)
42
+ * ```typescript
43
+ * import { apiCall, createMcpRouter, Server as McpServer } from '@ttoss/http-server-mcp';
44
+ *
45
+ * // Tool handler – no manual auth wiring needed
46
+ * mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
47
+ * const data = await apiCall('GET', '/portfolios');
48
+ * return { content: [{ type: 'text', text: JSON.stringify(data) }] };
49
+ * });
50
+ *
51
+ * const mcpRouter = createMcpRouter(mcpServer, {
52
+ * apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
53
+ * // Forward the caller's Bearer token to every apiCall
54
+ * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
55
+ * });
56
+ * ```
57
+ *
58
+ * @example x-api-key forwarding
59
+ * ```typescript
60
+ * const mcpRouter = createMcpRouter(mcpServer, {
61
+ * apiBaseUrl: 'https://internal-service/api',
62
+ * getApiHeaders: (ctx) => ({
63
+ * 'x-api-key': ctx.headers['x-api-key'] as string,
64
+ * }),
65
+ * });
66
+ * ```
67
+ *
68
+ * @example Third-party or public API (full URL, no context required)
69
+ * ```typescript
70
+ * const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
71
+ * const created = await apiCall('POST', 'https://api.example.com/items', {
72
+ * body: { name: 'widget' },
73
+ * headers: { Authorization: 'Bearer fixed-service-token' },
74
+ * });
75
+ * ```
76
+ */
77
+ declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
7
78
  /**
8
79
  * Options for configuring the MCP router
9
80
  */
@@ -14,10 +85,45 @@ interface McpRouterOptions {
14
85
  */
15
86
  path?: string;
16
87
  /**
17
- * Optional session ID generator for stateful MCP servers
18
- * Set to undefined for stateless servers
88
+ * Optional session ID generator for stateful MCP servers.
89
+ * When provided, a single shared transport is created and sessions are tracked.
90
+ * When undefined (default), the server operates in stateless mode where each
91
+ * HTTP request uses its own transport instance.
19
92
  */
20
93
  sessionIdGenerator?: () => string;
94
+ /**
95
+ * Base URL prepended to relative paths passed to `apiCall` (paths starting
96
+ * with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
97
+ * specifying a host.
98
+ *
99
+ * @example 'http://localhost:3000/api/v1'
100
+ */
101
+ apiBaseUrl?: string;
102
+ /**
103
+ * Called once per incoming MCP HTTP request. Return a plain object whose
104
+ * key-value pairs will be merged into the headers of every `apiCall` made
105
+ * within that request's tool handlers.
106
+ *
107
+ * Use this to forward any header from the MCP request — Bearer tokens, API
108
+ * keys, tenant IDs, trace headers, etc. — without coupling tool handlers to
109
+ * a specific authentication scheme.
110
+ *
111
+ * @example Forward a Bearer token
112
+ * ```typescript
113
+ * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' })
114
+ * ```
115
+ *
116
+ * @example Forward an x-api-key header
117
+ * ```typescript
118
+ * getApiHeaders: (ctx) => ({ 'x-api-key': ctx.headers['x-api-key'] as string })
119
+ * ```
120
+ *
121
+ * @example Inject a static service-to-service key
122
+ * ```typescript
123
+ * getApiHeaders: () => ({ 'x-internal-key': process.env.INTERNAL_API_KEY! })
124
+ * ```
125
+ */
126
+ getApiHeaders?: (ctx: Context) => Record<string, string>;
21
127
  }
22
128
  /**
23
129
  * Creates a Koa router configured to handle MCP protocol requests
@@ -58,4 +164,4 @@ interface McpRouterOptions {
58
164
  */
59
165
  declare const createMcpRouter: (server: McpServer, options?: McpRouterOptions) => _koa_router.RouterInstance<koa.DefaultState, koa.DefaultContext>;
60
166
 
61
- export { type McpRouterOptions, createMcpRouter };
167
+ export { type ApiCallOptions, type McpRouterOptions, apiCall, createMcpRouter };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,80 @@
1
1
  import * as _koa_router from '@koa/router';
2
2
  import * as koa from 'koa';
3
+ import { Context } from 'koa';
3
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
5
  export { McpServer, McpServer as Server } from '@modelcontextprotocol/sdk/server/mcp.js';
5
6
  export { z } from 'zod';
6
7
 
8
+ /**
9
+ * Options for a single `apiCall` request.
10
+ */
11
+ interface ApiCallOptions {
12
+ /**
13
+ * JSON-serialisable request body. Automatically serialised and sent with
14
+ * `Content-Type: application/json`.
15
+ */
16
+ body?: unknown;
17
+ /**
18
+ * Additional or override headers for this specific request.
19
+ * These are merged on top of any headers injected from the MCP request
20
+ * context via `getApiHeaders`, allowing per-call overrides.
21
+ */
22
+ headers?: Record<string, string>;
23
+ }
24
+ /**
25
+ * Generic HTTP helper for use inside MCP tool handlers.
26
+ *
27
+ * Accepts any full URL (third-party APIs, public APIs, etc.) or a path
28
+ * relative to the `apiBaseUrl` configured in `createMcpRouter`.
29
+ *
30
+ * Headers configured via `getApiHeaders` in `createMcpRouter` are injected
31
+ * automatically into every request, allowing transparent forwarding of auth
32
+ * tokens, API keys, or any other header — without coupling this helper to a
33
+ * specific authentication scheme. Per-call `options.headers` take precedence
34
+ * over context-injected headers.
35
+ *
36
+ * @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
37
+ * @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
38
+ * @param options - Optional body and per-call header overrides
39
+ * @returns Parsed JSON response body
40
+ *
41
+ * @example Bearer token forwarding (configured once in `createMcpRouter`)
42
+ * ```typescript
43
+ * import { apiCall, createMcpRouter, Server as McpServer } from '@ttoss/http-server-mcp';
44
+ *
45
+ * // Tool handler – no manual auth wiring needed
46
+ * mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
47
+ * const data = await apiCall('GET', '/portfolios');
48
+ * return { content: [{ type: 'text', text: JSON.stringify(data) }] };
49
+ * });
50
+ *
51
+ * const mcpRouter = createMcpRouter(mcpServer, {
52
+ * apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
53
+ * // Forward the caller's Bearer token to every apiCall
54
+ * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
55
+ * });
56
+ * ```
57
+ *
58
+ * @example x-api-key forwarding
59
+ * ```typescript
60
+ * const mcpRouter = createMcpRouter(mcpServer, {
61
+ * apiBaseUrl: 'https://internal-service/api',
62
+ * getApiHeaders: (ctx) => ({
63
+ * 'x-api-key': ctx.headers['x-api-key'] as string,
64
+ * }),
65
+ * });
66
+ * ```
67
+ *
68
+ * @example Third-party or public API (full URL, no context required)
69
+ * ```typescript
70
+ * const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
71
+ * const created = await apiCall('POST', 'https://api.example.com/items', {
72
+ * body: { name: 'widget' },
73
+ * headers: { Authorization: 'Bearer fixed-service-token' },
74
+ * });
75
+ * ```
76
+ */
77
+ declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
7
78
  /**
8
79
  * Options for configuring the MCP router
9
80
  */
@@ -14,10 +85,45 @@ interface McpRouterOptions {
14
85
  */
15
86
  path?: string;
16
87
  /**
17
- * Optional session ID generator for stateful MCP servers
18
- * Set to undefined for stateless servers
88
+ * Optional session ID generator for stateful MCP servers.
89
+ * When provided, a single shared transport is created and sessions are tracked.
90
+ * When undefined (default), the server operates in stateless mode where each
91
+ * HTTP request uses its own transport instance.
19
92
  */
20
93
  sessionIdGenerator?: () => string;
94
+ /**
95
+ * Base URL prepended to relative paths passed to `apiCall` (paths starting
96
+ * with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
97
+ * specifying a host.
98
+ *
99
+ * @example 'http://localhost:3000/api/v1'
100
+ */
101
+ apiBaseUrl?: string;
102
+ /**
103
+ * Called once per incoming MCP HTTP request. Return a plain object whose
104
+ * key-value pairs will be merged into the headers of every `apiCall` made
105
+ * within that request's tool handlers.
106
+ *
107
+ * Use this to forward any header from the MCP request — Bearer tokens, API
108
+ * keys, tenant IDs, trace headers, etc. — without coupling tool handlers to
109
+ * a specific authentication scheme.
110
+ *
111
+ * @example Forward a Bearer token
112
+ * ```typescript
113
+ * getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' })
114
+ * ```
115
+ *
116
+ * @example Forward an x-api-key header
117
+ * ```typescript
118
+ * getApiHeaders: (ctx) => ({ 'x-api-key': ctx.headers['x-api-key'] as string })
119
+ * ```
120
+ *
121
+ * @example Inject a static service-to-service key
122
+ * ```typescript
123
+ * getApiHeaders: () => ({ 'x-internal-key': process.env.INTERNAL_API_KEY! })
124
+ * ```
125
+ */
126
+ getApiHeaders?: (ctx: Context) => Record<string, string>;
21
127
  }
22
128
  /**
23
129
  * Creates a Koa router configured to handle MCP protocol requests
@@ -58,4 +164,4 @@ interface McpRouterOptions {
58
164
  */
59
165
  declare const createMcpRouter: (server: McpServer, options?: McpRouterOptions) => _koa_router.RouterInstance<koa.DefaultState, koa.DefaultContext>;
60
166
 
61
- export { type McpRouterOptions, createMcpRouter };
167
+ export { type ApiCallOptions, type McpRouterOptions, apiCall, createMcpRouter };
package/dist/index.js CHANGED
@@ -32,29 +32,129 @@ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  Server: () => import_mcp.McpServer,
35
+ apiCall: () => apiCall,
35
36
  createMcpRouter: () => createMcpRouter,
36
37
  z: () => import_zod.z
37
38
  });
38
39
  module.exports = __toCommonJS(index_exports);
40
+ var import_node_async_hooks = require("async_hooks");
39
41
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
40
42
  var import_http_server = require("@ttoss/http-server");
41
43
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
42
44
  var import_zod = require("zod");
45
+ var requestContextStore = new import_node_async_hooks.AsyncLocalStorage();
46
+ var apiCall = /* @__PURE__ */__name(async (method, url, options) => {
47
+ const context = requestContextStore.getStore();
48
+ let resolvedUrl = url;
49
+ if (url.startsWith("/")) {
50
+ if (!context?.apiBaseUrl) {
51
+ 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.`);
52
+ }
53
+ resolvedUrl = `${context.apiBaseUrl.replace(/\/$/, "")}${url}`;
54
+ }
55
+ const hasBody = options?.body !== void 0;
56
+ const headers = {
57
+ ...(context !== void 0 ? context.apiHeaders : {}),
58
+ ...(options?.headers ?? {})
59
+ };
60
+ const hasExplicitContentType = Object.keys(headers).some(headerName => {
61
+ return headerName.toLowerCase() === "content-type";
62
+ });
63
+ if (hasBody && !hasExplicitContentType) {
64
+ headers["Content-Type"] = "application/json";
65
+ }
66
+ const response = await fetch(resolvedUrl, {
67
+ method,
68
+ headers,
69
+ body: hasBody ? JSON.stringify(options.body) : void 0
70
+ });
71
+ if (!response.ok) {
72
+ const err = await response.json().catch(() => {
73
+ return {
74
+ error: response.statusText
75
+ };
76
+ });
77
+ throw new Error(err.error || `HTTP ${response.status}`);
78
+ }
79
+ if (response.status === 204 || response.status === 205) {
80
+ return void 0;
81
+ }
82
+ const contentType = response.headers.get("content-type");
83
+ if (contentType?.includes("application/json")) {
84
+ return response.json();
85
+ }
86
+ return response.text();
87
+ }, "apiCall");
43
88
  var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
44
89
  const {
45
90
  path = "/mcp",
46
- sessionIdGenerator
47
- } = options;
48
- const transport = new import_streamableHttp.StreamableHTTPServerTransport({
49
91
  sessionIdGenerator,
50
- enableJsonResponse: true
51
- });
52
- server.connect(transport);
92
+ apiBaseUrl,
93
+ getApiHeaders
94
+ } = options;
95
+ const isStateful = sessionIdGenerator !== void 0;
96
+ const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
97
+ let sharedTransport;
98
+ if (isStateful) {
99
+ sharedTransport = new import_streamableHttp.StreamableHTTPServerTransport({
100
+ sessionIdGenerator,
101
+ enableJsonResponse: true
102
+ });
103
+ server.connect(sharedTransport);
104
+ }
105
+ let statelessQueue = Promise.resolve();
106
+ const enqueueStateless = /* @__PURE__ */__name(work => {
107
+ const result = statelessQueue.then(() => {
108
+ return work();
109
+ });
110
+ statelessQueue = result.catch(() => {});
111
+ return result;
112
+ }, "enqueueStateless");
53
113
  const router = new import_http_server.Router();
114
+ const handleWithContext = /* @__PURE__ */__name(async (ctx, body) => {
115
+ const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
116
+ const runRequest = /* @__PURE__ */__name(async transport => {
117
+ await transport.handleRequest(ctx.req, ctx.res, body);
118
+ ctx.respond = false;
119
+ }, "runRequest");
120
+ if (isStateful && sharedTransport) {
121
+ if (needsContext) {
122
+ await requestContextStore.run({
123
+ apiBaseUrl,
124
+ apiHeaders
125
+ }, () => {
126
+ return runRequest(sharedTransport);
127
+ });
128
+ } else {
129
+ await runRequest(sharedTransport);
130
+ }
131
+ } else {
132
+ await enqueueStateless(async () => {
133
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({
134
+ sessionIdGenerator: void 0,
135
+ enableJsonResponse: true
136
+ });
137
+ try {
138
+ await server.connect(transport);
139
+ if (needsContext) {
140
+ await requestContextStore.run({
141
+ apiBaseUrl,
142
+ apiHeaders
143
+ }, () => {
144
+ return runRequest(transport);
145
+ });
146
+ } else {
147
+ await runRequest(transport);
148
+ }
149
+ } finally {
150
+ await transport.close();
151
+ }
152
+ });
153
+ }
154
+ }, "handleWithContext");
54
155
  router.post(path, async ctx => {
55
156
  try {
56
- await transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
57
- ctx.respond = false;
157
+ await handleWithContext(ctx, ctx.request.body);
58
158
  } catch (error) {
59
159
  if (!ctx.res.headersSent) {
60
160
  ctx.status = 500;
@@ -66,8 +166,7 @@ var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
66
166
  });
67
167
  router.delete(path, async ctx => {
68
168
  try {
69
- await transport.handleRequest(ctx.req, ctx.res, void 0);
70
- ctx.respond = false;
169
+ await handleWithContext(ctx);
71
170
  } catch (error) {
72
171
  if (!ctx.res.headersSent) {
73
172
  ctx.status = 500;
@@ -82,6 +181,7 @@ var createMcpRouter = /* @__PURE__ */__name((server, options = {}) => {
82
181
  // Annotate the CommonJS export names for ESM import in node:
83
182
  0 && (module.exports = {
84
183
  Server,
184
+ apiCall,
85
185
  createMcpRouter,
86
186
  z
87
187
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "license": "MIT",
6
6
  "author": "ttoss",
@@ -25,16 +25,16 @@
25
25
  ],
26
26
  "sideEffects": false,
27
27
  "dependencies": {
28
- "@modelcontextprotocol/sdk": "^1.0.4",
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
29
  "zod": "^3.25.0",
30
- "@ttoss/http-server": "^0.5.7"
30
+ "@ttoss/http-server": "^0.5.8"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/koa": "^3.0.1",
34
34
  "jest": "^30.3.0",
35
35
  "supertest": "^7.1.4",
36
36
  "tsup": "^8.5.1",
37
- "@ttoss/config": "^1.37.7"
37
+ "@ttoss/config": "^1.37.8"
38
38
  },
39
39
  "keywords": [
40
40
  "ai",