@ganintegrity/mcp 1.0.0 → 1.1.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.
Files changed (80) hide show
  1. package/README.md +186 -60
  2. package/dist/core/als.d.ts.map +1 -0
  3. package/dist/core/als.js.map +1 -0
  4. package/dist/core/auth/auth.types.d.ts.map +1 -0
  5. package/dist/{auth → core/auth}/auth.types.js.map +1 -1
  6. package/dist/core/auth/index.d.ts +33 -0
  7. package/dist/core/auth/index.d.ts.map +1 -0
  8. package/dist/core/auth/index.js +57 -0
  9. package/dist/core/auth/index.js.map +1 -0
  10. package/dist/core/define.d.ts +64 -0
  11. package/dist/core/define.d.ts.map +1 -0
  12. package/dist/core/define.js +48 -0
  13. package/dist/core/define.js.map +1 -0
  14. package/dist/core/dispatch.d.ts +34 -0
  15. package/dist/core/dispatch.d.ts.map +1 -0
  16. package/dist/core/dispatch.js +57 -0
  17. package/dist/core/dispatch.js.map +1 -0
  18. package/dist/core/errors/errors.types.d.ts.map +1 -0
  19. package/dist/core/errors/errors.types.js.map +1 -0
  20. package/dist/core/errors/index.d.ts.map +1 -0
  21. package/dist/core/errors/index.js.map +1 -0
  22. package/dist/core/test-helpers.d.ts +20 -0
  23. package/dist/core/test-helpers.d.ts.map +1 -0
  24. package/dist/core/test-helpers.js +38 -0
  25. package/dist/core/test-helpers.js.map +1 -0
  26. package/dist/core/tool/index.d.ts +19 -0
  27. package/dist/core/tool/index.d.ts.map +1 -0
  28. package/dist/{tool → core/tool}/index.js +28 -25
  29. package/dist/core/tool/index.js.map +1 -0
  30. package/dist/{tool → core/tool}/tool.types.d.ts +21 -0
  31. package/dist/core/tool/tool.types.d.ts.map +1 -0
  32. package/dist/{tool → core/tool}/tool.types.js.map +1 -1
  33. package/dist/{auth/index.d.ts → express/auth.d.ts} +4 -5
  34. package/dist/express/auth.d.ts.map +1 -0
  35. package/dist/express/auth.js +47 -0
  36. package/dist/express/auth.js.map +1 -0
  37. package/dist/express/express.types.d.ts +35 -0
  38. package/dist/express/express.types.d.ts.map +1 -0
  39. package/dist/express/express.types.js +2 -0
  40. package/dist/express/express.types.js.map +1 -0
  41. package/dist/express/index.d.ts +32 -0
  42. package/dist/express/index.d.ts.map +1 -0
  43. package/dist/express/index.js +50 -0
  44. package/dist/express/index.js.map +1 -0
  45. package/dist/index.d.ts +13 -18
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -4
  48. package/dist/index.js.map +1 -1
  49. package/package.json +5 -1
  50. package/dist/als.d.ts.map +0 -1
  51. package/dist/als.js.map +0 -1
  52. package/dist/auth/auth.types.d.ts.map +0 -1
  53. package/dist/auth/index.d.ts.map +0 -1
  54. package/dist/auth/index.js +0 -74
  55. package/dist/auth/index.js.map +0 -1
  56. package/dist/errors/errors.types.d.ts.map +0 -1
  57. package/dist/errors/errors.types.js.map +0 -1
  58. package/dist/errors/index.d.ts.map +0 -1
  59. package/dist/errors/index.js.map +0 -1
  60. package/dist/server/index.d.ts +0 -18
  61. package/dist/server/index.d.ts.map +0 -1
  62. package/dist/server/index.js +0 -130
  63. package/dist/server/index.js.map +0 -1
  64. package/dist/server/server.types.d.ts +0 -78
  65. package/dist/server/server.types.d.ts.map +0 -1
  66. package/dist/server/server.types.js +0 -2
  67. package/dist/server/server.types.js.map +0 -1
  68. package/dist/tool/index.d.ts +0 -26
  69. package/dist/tool/index.d.ts.map +0 -1
  70. package/dist/tool/index.js.map +0 -1
  71. package/dist/tool/tool.types.d.ts.map +0 -1
  72. /package/dist/{als.d.ts → core/als.d.ts} +0 -0
  73. /package/dist/{als.js → core/als.js} +0 -0
  74. /package/dist/{auth → core/auth}/auth.types.d.ts +0 -0
  75. /package/dist/{auth → core/auth}/auth.types.js +0 -0
  76. /package/dist/{errors → core/errors}/errors.types.d.ts +0 -0
  77. /package/dist/{errors → core/errors}/errors.types.js +0 -0
  78. /package/dist/{errors → core/errors}/index.d.ts +0 -0
  79. /package/dist/{errors → core/errors}/index.js +0 -0
  80. /package/dist/{tool → core/tool}/tool.types.js +0 -0
package/README.md CHANGED
@@ -26,15 +26,15 @@ This library packages (1)–(4) and asks the caller to plug in the parts that ge
26
26
 
27
27
  A two-minute mental model before you read the API:
28
28
 
29
- ### The recording shim
29
+ ### Define-once, materialize-per-request
30
30
 
31
- `createMcpServer()` returns a `server` object that _looks_ like an `McpServer` but only implements `registerTool` — it records every registration in an in-memory list. On each HTTP request, `mount()` instantiates a brand-new `McpServer`, replays the recorded registrations onto it, hands it a fresh transport, and tears both down when the response finishes. This is how we satisfy the SDK's single-use transport contract without re-registering tools on every call site.
31
+ `defineMcpServer({ tools })` runs your `tools(register)` callback once and captures each `register.tool(spec)` invocation onto an `McpDefinition`. On each HTTP request, the framework adapter calls `materializeSdkServer(mcp)` to build a fresh SDK `McpServer`, replays the captured registrations onto it, hands it a fresh transport, and tears both down when the response finishes. This is how we satisfy the SDK's single-use transport contract without re-registering tools on every call site.
32
32
 
33
- You register tools once at boot time. The library handles the per-request lifecycle.
33
+ You write tool registrations once. The library handles the per-request lifecycle.
34
34
 
35
35
  ### The ALS scope
36
36
 
37
- Inside `mount()`, every tool dispatch runs inside an `AsyncLocalStorage.run(...)` scope holding a `RequestStore` (user, postgan, sessionId, logger). The `tool()` helper reads from this store to build a `ToolContext` for your handler. Your handler signature is `(args, ctx) => Promise<Output>` — no Express objects leak in, no DI container needed.
37
+ Every tool dispatch runs inside an `AsyncLocalStorage.run(...)` scope holding a `RequestStore` (user, postgan, sessionId, logger). The wrapper that `register.tool()` builds reads from this store to assemble a `ToolContext` for your handler. Your handler signature is `(args, ctx) => Promise<Output>` — no Express objects leak in, no DI container needed.
38
38
 
39
39
  ### Per-tool transactions
40
40
 
@@ -71,7 +71,8 @@ The library does not bundle any of these — your service is expected to already
71
71
  ```ts
72
72
  // src/mcp/bootstrap.ts
73
73
  import express from "express";
74
- import { createMcpServer, tool, type McpErrorMapper } from "@ganintegrity/mcp";
74
+ import { defineMcpServer, type McpErrorMapper } from "@ganintegrity/mcp";
75
+ import { createMcpRouter } from "@ganintegrity/mcp/express";
75
76
  import { z } from "zod";
76
77
 
77
78
  import { logger } from "../logger.ts";
@@ -93,80 +94,178 @@ const errorMapper: McpErrorMapper = (err) => {
93
94
  return null;
94
95
  };
95
96
 
96
- const { server, mount } = createMcpServer({
97
+ // Register tools at boot, inside the `tools` callback. The callback runs
98
+ // once at define time; the registrations are replayed onto a fresh SDK
99
+ // `McpServer` per HTTP request.
100
+ const mcp = defineMcpServer({
97
101
  name: "tprm-mcp",
98
102
  version: "1.0.0",
103
+ errorMapper,
104
+ tools(register) {
105
+ register.tool({
106
+ name: "summarise_entity",
107
+ description: "Produce a short summary of an entity by uuid.",
108
+ inputSchema: z.object({ uuid: z.string().uuid() }),
109
+ handler: async ({ uuid }, ctx) => {
110
+ const entity = await ctx.postgan.entity.fetch({ uuid });
111
+ return { summary: entity.name, fetchedBy: ctx.user.id };
112
+ },
113
+ });
114
+ },
115
+ });
116
+
117
+ // Mount onto an Express app.
118
+ const mcpRouter = createMcpRouter(mcp, {
99
119
  logger,
100
120
  setupPostgan: setupPostgan(),
101
121
  tokenToUser: async (token) => {
102
122
  const decoded = await decipherToken(token);
103
123
  return identity.construct({ ...decoded, id: decoded._id });
104
124
  },
125
+ });
126
+ app.use("/mcp", mcpRouter);
127
+ ```
128
+
129
+ That's it. The MCP server is reachable at `POST /mcp/`, authenticates via `Authorization: Bearer <token>`, and executes tools inside a postgan transaction with full ALS context.
130
+
131
+ ---
132
+
133
+ ## Framework adapters
134
+
135
+ The library is split into a framework-neutral core and per-framework adapters. Consumers import from a subpath that matches their HTTP framework.
136
+
137
+ | Import | Status | Use |
138
+ | --------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
139
+ | `@ganintegrity/mcp` | Implemented | Framework-neutral surface: `defineMcpServer`, `materializeSdkServer`, error envelope plumbing, every shared type. |
140
+ | `@ganintegrity/mcp/express` | Implemented | Express ^5 adapter: `createMcpRouter`, `mcpAuth`, `CreateMcpRouterOptions`. |
141
+ | `@ganintegrity/mcp/koa` | Not yet built | Koa ^2 adapter; intended API documented below. Same names against Koa types. |
142
+
143
+ A bootstrap file imports from both — framework-neutral helpers from the root, the adapter factory from `/express`. A tool definition file imports from the root only; it stays adapter-agnostic.
144
+
145
+ Importing from `/express` (or `/koa`, when implemented) loads the framework's ambient type augmentation (e.g. `Express.Request` gains `mcpSessionId` / `auth`). The root entry has no such side effects — services that don't run Express won't see Express types in their globals.
146
+
147
+ The shared core lives under `src/core/` and contains:
148
+
149
+ - **`defineMcpServer({ name, version, errorMapper, tools })`** — the public entry point. Captures tool registrations into an `McpDefinition` once at define time.
150
+ - **`materializeSdkServer(mcp): McpServer`** — builds a fresh SDK `McpServer` and replays the captured registrations onto it. Adapters call this per HTTP request (the SDK's stateless transport is single-use). Exposed for tests + advanced consumers writing custom adapters.
151
+ - **`resolveAuth(headers, options): AuthResult`** — extracts the bearer token + session id from a header bag, calls `tokenToUser`, returns a discriminated success/failure union.
152
+ - **`dispatchMcpRequest(mcp, logger, scope, invokeTransport, onTransportError)`** — owns the per-request lifecycle: builds the `RequestStore`, materializes the SDK server, runs the framework-supplied transport invocation inside `requestStore.run(...)`, tears everything down on `finally`.
153
+ - The error envelope plumbing (`toCallToolError`, `INTERNAL_ERROR`) and the postgan-transaction wrapper.
154
+
155
+ Each adapter is a thin layer that bridges its native request/response shape to these neutral helpers.
156
+
157
+ ### Intended Koa API
158
+
159
+ When the Koa adapter is built, it mirrors the Express one:
160
+
161
+ ```ts
162
+ // src/mcp/bootstrap.ts
163
+ import Koa from "koa";
164
+ import Router from "@koa/router";
165
+ import bodyParser from "@koa/bodyparser";
166
+
167
+ import { defineMcpServer, type McpErrorMapper } from "@ganintegrity/mcp";
168
+ import { createMcpRouter } from "@ganintegrity/mcp/koa";
169
+
170
+ const mcp = defineMcpServer({
171
+ name: "tprm-mcp",
172
+ version: "1.0.0",
105
173
  errorMapper,
174
+ tools(register) {
175
+ register.tool({
176
+ /* identical to express — same ToolContext, same handler signature */
177
+ });
178
+ },
106
179
  });
107
180
 
108
- // Register tools at boot.
109
- tool(server, {
110
- name: "summarise_entity",
111
- description: "Produce a short summary of an entity by uuid.",
112
- inputSchema: z.object({ uuid: z.string().uuid() }),
113
- handler: async ({ uuid }, ctx) => {
114
- const entity = await ctx.postgan.entity.fetch({ uuid });
115
- return { summary: entity.name, fetchedBy: ctx.user.id };
181
+ const mcpRouter = createMcpRouter(mcp, {
182
+ logger,
183
+ setupPostgan: setupPostganKoa(), // Koa.Middleware that sets ctx.state.postgan
184
+ tokenToUser: async (token) => {
185
+ /* same shape as express */
116
186
  },
117
187
  });
118
188
 
119
- // Mount onto an Express router.
120
- const router = express.Router();
121
- await mount(router);
122
- app.use("/mcp", router);
189
+ const app = new Koa();
190
+ app.use(bodyParser()); // caller-provided; the library does not bundle one
191
+ app.use(mcpRouter.routes()).use(mcpRouter.allowedMethods());
123
192
  ```
124
193
 
125
- That's it. The MCP server is reachable at `POST /mcp/`, will authenticate via `Authorization: Bearer <token>` (or `X-Access-Token`), and will execute tools inside a postgan transaction with full ALS context.
194
+ Differences from the Express adapter the implementation needs to handle:
195
+
196
+ - **`setupPostgan: Koa.Middleware`** — sets `ctx.state.postgan` instead of `req.postgan`. The auth middleware sets `ctx.state.user` (and `ctx.state.mcpSessionId` if present).
197
+ - **`mcpAuth(options): Koa.Middleware`** — parallel to the Express middleware. Same options shape (`tokenToUser`, `logger`); internally calls `resolveAuth(ctx.headers, options)` and either responds 401 (`ctx.status` / `ctx.body`) or writes onto `ctx.state` and calls `next()`.
198
+ - **`createMcpRouter(mcp, options)`** — returns a fresh `@koa/router` Router with the JSON-RPC route attached; the consumer mounts it via `app.use(router.routes()).use(router.allowedMethods())`.
199
+ - **Body parser is the consumer's responsibility.** Koa has no built-in JSON parser. The Express adapter mounts `express.json()` automatically; the Koa adapter expects the caller to mount `bodyParser()` (or equivalent) on the Koa app upstream of the MCP router.
200
+ - **`handleMcpRequest`** — the dispatcher's `invokeTransport` callback becomes:
201
+ ```ts
202
+ (transport) => {
203
+ ctx.respond = false; // opt out of Koa's response handling
204
+ return transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
205
+ };
206
+ ```
207
+ Koa's `ctx.req` / `ctx.res` are the underlying Node `IncomingMessage` / `ServerResponse`, which is what the MCP SDK transport expects.
208
+ - **`onTransportError`** — `() => { if (!ctx.headerSent) { ctx.status = 500; ctx.body = { error: "Internal MCP transport error" }; } }`.
209
+ - **No global type augmentation.** The Express adapter augments `Express.Request` with `mcpSessionId` / `auth`; Koa uses typed `ctx.state<T>`. The Koa adapter exports a `KoaState` interface consumers can compose with their own state typing.
210
+
211
+ Tool handlers see the same `ToolContext` regardless of adapter — the framework-specific bits stop at the auth + dispatch boundary.
212
+
213
+ ### Planned Koa peer dependencies
214
+
215
+ When the Koa adapter ships, these peers will be added (only required if you actually import the `/koa` subpath):
216
+
217
+ | Peer | Range | Why |
218
+ | ------------- | ----- | ----------------------------------- |
219
+ | `koa` | `^2` | adapter is built against it. |
220
+ | `@koa/router` | `^13` | `createMcpRouter` returns a Router. |
221
+
222
+ `@koa/bodyparser` is **not** a peer — the consumer chooses and mounts a body parser on their app.
126
223
 
127
224
  ---
128
225
 
129
226
  ## API reference
130
227
 
131
- ### `createMcpServer(options)`
228
+ > Each export below is annotated with the subpath it lives on — `@ganintegrity/mcp` (framework-neutral) or `@ganintegrity/mcp/express` (Express adapter). See [Framework adapters](#framework-adapters) for the full split.
132
229
 
133
- Builds an MCP server and returns a `{ server, mount }` pair.
230
+ ### `defineMcpServer(options)` `@ganintegrity/mcp`
134
231
 
135
- **Options:**
232
+ Captures an MCP server definition. The `tools` callback runs **once at define time**; the `register.tool(spec)` invocations inside it are recorded and replayed onto a fresh SDK `McpServer` per HTTP request.
136
233
 
137
- | Field | Type | Description |
138
- | -------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
139
- | `name` | `string` | MCP server name advertised to clients. |
140
- | `version` | `string` | MCP server version advertised to clients. |
141
- | `logger` | `pino.Logger` | Service logger. The library calls `logger.child({ component: "mcp" })` internally. |
142
- | `tokenToUser` | `(token: string) => Promise<AuthUser>` | Resolve a bearer token to a user. The library is neutral about how tokens are verified — plug in your service's existing JWT/cipher/identity pipeline. |
143
- | `setupPostgan` | `RequestHandler` | Express middleware that attaches a `Postgan` instance to `req.postgan`. The library mounts this between auth and the JSON-RPC dispatcher. |
144
- | `errorMapper?` | `McpErrorMapper` | Translate a thrown error into an MCP envelope. Return `null` for the redacted `INTERNAL_ERROR` fallback. Without a mapper, every thrown error becomes `INTERNAL_ERROR` with the message redacted. |
234
+ **Options:**
145
235
 
146
- **Returns:**
236
+ | Field | Type | Description |
237
+ | -------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `name` | `string` | MCP server name advertised to clients. |
239
+ | `version` | `string` | MCP server version advertised to clients. |
240
+ | `tools` | `(register: ToolRegister) => void` | Registration callback. Call `register.tool(spec)` once per tool. Anything beyond registration runs once at define time, not per request. |
241
+ | `errorMapper?` | `McpErrorMapper` | Translate a thrown error into an MCP envelope. Return `null` for the redacted `INTERNAL_ERROR` fallback. Without a mapper, every thrown error becomes `INTERNAL_ERROR`. |
147
242
 
148
- | Field | Type | Description |
149
- | -------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
150
- | `server` | `McpServer` (recording shim) | Pass to `tool()` to register tools. Only `registerTool` is exposed; other `McpServer` methods are not implemented. |
151
- | `mount` | `(target: Router) => Promise<void>` | Attaches the MCP route handlers to an Express router. Caller decides the mount path. |
243
+ **Returns:** an opaque `McpDefinition` you hand to a per-framework adapter (`createMcpRouter`, etc.). Don't inspect or mutate it — adapters and `materializeSdkServer` are the only consumers.
152
244
 
153
- ### `tool(server, spec)`
245
+ ### `register.tool(spec)` — `@ganintegrity/mcp`
154
246
 
155
- Register a tool. The handler runs inside an ALS scope with a fresh postgan transaction.
247
+ The method on the `register` argument inside `defineMcpServer`'s `tools` callback. Wraps your handler with ALS scope, an auto-committed postgan transaction, and the error envelope pipeline before recording it onto the definition.
156
248
 
157
249
  ```ts
158
- tool(server, {
159
- name: "tool_name",
160
- description: "What this tool does",
161
- inputSchema: z.object({
162
- /* ... */
163
- }),
164
- annotations: { readOnlyHint: true }, // optional
165
- handler: async (args, ctx) => {
166
- // ctx.user, ctx.postgan, ctx.transaction, ctx.sessionId, ctx.logger
167
- return {
168
- /* plain object — surfaced as both text and structuredContent */
169
- };
250
+ const mcp = defineMcpServer({
251
+ name: "tprm-mcp",
252
+ version: "1.0.0",
253
+ errorMapper,
254
+ tools(register) {
255
+ register.tool({
256
+ name: "tool_name",
257
+ description: "What this tool does",
258
+ inputSchema: z.object({
259
+ /* ... */
260
+ }),
261
+ annotations: { readOnlyHint: true }, // optional
262
+ handler: async (args, ctx) => {
263
+ // ctx.user, ctx.postgan, ctx.transaction, ctx.sessionId, ctx.logger
264
+ return {
265
+ /* plain object — surfaced as both text and structuredContent */
266
+ };
267
+ },
268
+ });
170
269
  },
171
270
  });
172
271
  ```
@@ -191,9 +290,34 @@ tool(server, {
191
290
  | `idempotentHint` | Calling twice with same args is safe |
192
291
  | `openWorldHint` | Tool reaches out to external systems |
193
292
 
194
- ### `mcpAuth(options)`
293
+ ### `createMcpRouter(mcp, options)` — `@ganintegrity/mcp/express`
294
+
295
+ Build an Express `Router` that serves the supplied `McpDefinition`. Mount on whatever path the caller likes:
195
296
 
196
- The auth middleware that `createMcpServer` mounts internally. Exported in case you need to compose it differently.
297
+ ```ts
298
+ const mcpRouter = createMcpRouter(mcp, {
299
+ logger,
300
+ setupPostgan: setupPostgan(),
301
+ tokenToUser: async (token) => {
302
+ /* ... */
303
+ },
304
+ });
305
+ app.use("/mcp", mcpRouter);
306
+ ```
307
+
308
+ **Options:**
309
+
310
+ | Field | Type | Description |
311
+ | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
312
+ | `logger` | `pino.Logger` | Service logger. The library calls `logger.child({ component: "mcp" })` internally and further childs per request and per tool call. |
313
+ | `tokenToUser` | `(token: string) => Promise<AuthUser>` | Resolve a bearer token to a user. Reject by throwing — the router responds 401. |
314
+ | `setupPostgan` | `RequestHandler` | Express middleware that attaches a `Postgan` instance to `req.postgan`. The router mounts it between auth and the JSON-RPC dispatcher. |
315
+
316
+ **Returns:** an `express.Router` with `express.json()` → `mcpAuth` → `setupPostgan` → JSON-RPC dispatch composed onto it. Each request runs through the chain once.
317
+
318
+ ### `mcpAuth(options)` — `@ganintegrity/mcp/express`
319
+
320
+ The auth middleware that `createMcpRouter` mounts internally. Exported in case you need to compose it differently.
197
321
 
198
322
  ```ts
199
323
  mcpAuth({
@@ -206,7 +330,7 @@ mcpAuth({
206
330
 
207
331
  Sets `req.user`, `req.headers.company`, `req.auth` (SDK-shaped `AuthInfo`), and optionally `req.mcpSessionId` (from `X-Session-Id` header).
208
332
 
209
- ### Errors
333
+ ### Errors — `@ganintegrity/mcp`
210
334
 
211
335
  The library deliberately does **not** define an error class. Your service throws whatever it already throws (an `AppError`, a `ZodError`, anything), and you supply an `McpErrorMapper` callback to translate those throwables into MCP envelopes at the tool boundary.
212
336
 
@@ -231,9 +355,9 @@ import {
231
355
  ```
232
356
  - **`McpErrorMapper`** — `(err: unknown) => McpToolError | null`. Return `null` to fall through to a redacted `INTERNAL_ERROR` envelope (the original message is **not** leaked to the agent).
233
357
  - **`INTERNAL_ERROR`** — string constant, the only code the library itself emits.
234
- - **`toCallToolError(mapped, originalErr, meta)`** — used internally by `tool()`. Exported in case you build your own tool wrappers.
358
+ - **`toCallToolError(mapped, originalErr, meta)`** — used internally by `register.tool()`. Exported in case you build your own tool wrappers.
235
359
 
236
- The `tool()` helper invokes the mapper and `toCallToolError` automatically when a handler throws — you typically don't call them directly.
360
+ The `register.tool()` wrapper invokes the mapper and `toCallToolError` automatically when a handler throws — you typically don't call them directly.
237
361
 
238
362
  ---
239
363
 
@@ -316,13 +440,15 @@ Mapping rules to keep in mind:
316
440
 
317
441
  ## Architecture deep dive
318
442
 
319
- ### Why a recording shim?
443
+ ### Why `defineMcpServer` + `materializeSdkServer` instead of `new McpServer()`?
320
444
 
321
445
  The natural shape would be: `const server = new McpServer(...); server.registerTool(...); app.post("/mcp", (req, res) => transport.handleRequest(req, res, req.body))`. This breaks because `StreamableHTTPServerTransport` in stateless mode is single-use — once it has handled one request, it cannot be reused. The MCP SDK enforces this internally.
322
446
 
323
447
  The fix is to instantiate a transport per request. But then the `McpServer` would also need to be per-request (the SDK couples the two via `connect()`), which means re-registering every tool on every request.
324
448
 
325
- The recording shim solves this: tools are registered once into a list, and `mount()` replays the list onto a fresh `McpServer` per request. The replay is cheap — `registerTool` just stores function references, and the McpServer constructor is similarly lightweight. End-to-end latency is dominated by the actual tool work.
449
+ The library splits the two concerns: `defineMcpServer` captures registrations once into an `McpDefinition`; `materializeSdkServer(mcp)` builds a fresh SDK server with the same tools per request. The replay is cheap — `registerTool` just stores function references, and the `McpServer` constructor is similarly lightweight. End-to-end latency is dominated by the actual tool work.
450
+
451
+ The earlier shape used a "recording shim" — a fake `McpServer` whose only working method was `registerTool`. That dishonesty (it looked like an `McpServer`, but `.close()` and friends would have failed) is what `defineMcpServer` replaces.
326
452
 
327
453
  ### Why AsyncLocalStorage instead of passing context?
328
454
 
@@ -331,7 +457,7 @@ Tools could receive the `Request`/`Response` directly, and pull `user`/`postgan`
331
457
  1. Couples every tool to Express. If the MCP server ever needs to be invoked over a different transport (a worker, a CLI, a test), every tool breaks.
332
458
  2. Forces every helper called by a tool to also receive the context, polluting helper signatures throughout the service.
333
459
 
334
- ALS lets tool helpers reach context anywhere down the call stack without parameter threading, while keeping the tool-handler signature `(args, ctx)` clean and transport-agnostic. The `requestStore.run(...)` boundary in `mount()` is the only place ALS knows about Express.
460
+ ALS lets tool helpers reach context anywhere down the call stack without parameter threading, while keeping the tool-handler signature `(args, ctx)` clean and transport-agnostic. The `requestStore.run(...)` boundary in `dispatchMcpRequest` is the only place ALS knows about Express.
335
461
 
336
462
  ### Why per-tool transactions?
337
463
 
@@ -339,15 +465,15 @@ A single MCP request can dispatch multiple tool calls. Wrapping the entire reque
339
465
 
340
466
  The cost is more transaction begin/commits, which on a healthy postgres is negligible.
341
467
 
342
- ### Why is `tool()` separate from the `McpServer` returned by `createMcpServer`?
468
+ ### Why `register.tool(spec)` instead of `server.registerTool(...)`?
343
469
 
344
- `tool()` is a thin wrapper that:
470
+ `register.tool(spec)` is a thin wrapper around the SDK's `registerTool` that:
345
471
 
346
472
  1. Reads the ALS store to build a `ToolContext`.
347
473
  2. Opens a transaction, runs your handler, commits or rolls back.
348
474
  3. Maps thrown errors to `CallToolResult` envelopes.
349
475
 
350
- You could call `server.registerTool(...)` directly and skip all of this — useful if you want raw access to the SDK's request/response shape. But you'd lose ALS, transactions, and the error mapping. `tool()` is what makes the rest of the library useful; `server.registerTool` is the escape hatch.
476
+ You could call `materializeSdkServer(mcp).registerTool(...)` directly and skip all of this — useful if you want raw access to the SDK's request/response shape. But you'd lose ALS, transactions, and the error mapping. `register.tool()` is what makes the rest of the library useful; the SDK API is the escape hatch.
351
477
 
352
478
  ---
353
479
 
@@ -0,0 +1 @@
1
+ {"version":3,"file":"als.d.ts","sourceRoot":"","sources":["../../src/core/als.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,iCAAwC,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"als.js","sourceRoot":"","sources":["../../src/core/als.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAyBrD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,iBAAiB,EAAgB,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.types.d.ts","sourceRoot":"","sources":["../../../src/core/auth/auth.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClD,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.types.js","sourceRoot":"","sources":["../../src/auth/auth.types.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"auth.types.js","sourceRoot":"","sources":["../../../src/core/auth/auth.types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import type { AuthUser, McpAuthOptions } from "./auth.types.ts";
2
+ /**
3
+ * Framework-neutral header bag. Both Express's `req.headers` and Koa's
4
+ * `ctx.headers` satisfy this shape — Node lowercases the keys at the HTTP
5
+ * layer, so `"authorization"` and `"x-session-id"` reads work for either.
6
+ */
7
+ export type HeaderBag = Record<string, string | string[] | undefined>;
8
+ export type AuthResult = {
9
+ ok: true;
10
+ user: AuthUser;
11
+ token: string;
12
+ sessionId?: string;
13
+ } | {
14
+ ok: false;
15
+ status: 401;
16
+ message: string;
17
+ };
18
+ /**
19
+ * Resolve a request's auth from its headers. Reads `Authorization: Bearer
20
+ * <token>` and (optionally) `X-Session-Id`, then calls `tokenToUser` to
21
+ * resolve the token to an {@link AuthUser}.
22
+ *
23
+ * Returns a discriminated union — never throws. Per-framework adapters
24
+ * translate the result into a 401 response or a request-augmenting `next()`.
25
+ *
26
+ * - Missing/invalid Authorization header → `{ ok: false, status: 401,
27
+ * message: "Missing or invalid Authorization header" }`.
28
+ * - `tokenToUser` throws → `{ ok: false, status: 401, message:
29
+ * "Authentication failed: <reason>" }`. The throwable is logged at `warn`
30
+ * on `options.logger` before the result is returned.
31
+ */
32
+ export declare function resolveAuth(headers: HeaderBag, options: McpAuthOptions): Promise<AuthResult>;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEtE,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAiBhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC,CA0BrB"}
@@ -0,0 +1,57 @@
1
+ const BEARER_PREFIX = /^Bearer\s+/i;
2
+ function extractBearer(headers) {
3
+ const auth = headers["authorization"];
4
+ if (typeof auth === "string" && BEARER_PREFIX.test(auth)) {
5
+ return auth.replace(BEARER_PREFIX, "").trim();
6
+ }
7
+ return undefined;
8
+ }
9
+ function extractSessionId(headers) {
10
+ const sessionId = headers["x-session-id"];
11
+ return typeof sessionId === "string" && sessionId.length > 0
12
+ ? sessionId
13
+ : undefined;
14
+ }
15
+ /**
16
+ * Resolve a request's auth from its headers. Reads `Authorization: Bearer
17
+ * <token>` and (optionally) `X-Session-Id`, then calls `tokenToUser` to
18
+ * resolve the token to an {@link AuthUser}.
19
+ *
20
+ * Returns a discriminated union — never throws. Per-framework adapters
21
+ * translate the result into a 401 response or a request-augmenting `next()`.
22
+ *
23
+ * - Missing/invalid Authorization header → `{ ok: false, status: 401,
24
+ * message: "Missing or invalid Authorization header" }`.
25
+ * - `tokenToUser` throws → `{ ok: false, status: 401, message:
26
+ * "Authentication failed: <reason>" }`. The throwable is logged at `warn`
27
+ * on `options.logger` before the result is returned.
28
+ */
29
+ export async function resolveAuth(headers, options) {
30
+ const token = extractBearer(headers);
31
+ if (!token) {
32
+ return {
33
+ ok: false,
34
+ status: 401,
35
+ message: "Missing or invalid Authorization header",
36
+ };
37
+ }
38
+ try {
39
+ const user = await options.tokenToUser(token);
40
+ return {
41
+ ok: true,
42
+ user,
43
+ token,
44
+ sessionId: extractSessionId(headers),
45
+ };
46
+ }
47
+ catch (err) {
48
+ const reason = err instanceof Error ? err.message : "invalid token";
49
+ options.logger.warn({ err }, "mcp auth: token rejected");
50
+ return {
51
+ ok: false,
52
+ status: 401,
53
+ message: `Authentication failed: ${reason}`,
54
+ };
55
+ }
56
+ }
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/auth/index.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAG,aAAa,CAAC;AAapC,SAAS,aAAa,CAAC,OAAkB;IACvC,MAAM,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAkB;IAC1C,MAAM,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1C,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAC1D,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAkB,EAClB,OAAuB;IAEvB,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,yCAAyC;SACnD,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO;YACL,EAAE,EAAE,IAAI;YACR,IAAI;YACJ,KAAK;YACL,SAAS,EAAE,gBAAgB,CAAC,OAAO,CAAC;SACrC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;QACpE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,0BAA0B,CAAC,CAAC;QACzD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,0BAA0B,MAAM,EAAE;SAC5C,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpErrorMapper } from "./errors/errors.types.ts";
3
+ import type { ToolRegister, ToolRegistration } from "./tool/tool.types.ts";
4
+ /**
5
+ * Options for {@link defineMcpServer}.
6
+ */
7
+ export interface DefineMcpServerOptions {
8
+ /** MCP server name advertised to clients during initialisation. */
9
+ name: string;
10
+ /** MCP server version advertised to clients during initialisation. */
11
+ version: string;
12
+ /**
13
+ * Registration callback. Called **once at define time** with a
14
+ * `register` object whose `tool(spec)` method captures each tool into
15
+ * an internal array. Adapters replay the array onto a fresh `McpServer`
16
+ * per HTTP request.
17
+ *
18
+ * Don't put expensive setup in here — it's intended only for
19
+ * `register.tool(...)` calls.
20
+ */
21
+ tools: (register: ToolRegister) => void;
22
+ /**
23
+ * Translate a thrown error into an MCP envelope. Return `null` to fall
24
+ * through to a redacted `INTERNAL_ERROR` envelope. Without a mapper,
25
+ * every thrown error becomes `INTERNAL_ERROR` with the message redacted.
26
+ */
27
+ errorMapper?: McpErrorMapper;
28
+ }
29
+ /**
30
+ * The serialisable shape adapters consume. Holds the captured tool
31
+ * registrations alongside the metadata needed to materialise an SDK server
32
+ * per request. Treat as opaque outside of adapters and tests.
33
+ */
34
+ export interface McpDefinition {
35
+ readonly name: string;
36
+ readonly version: string;
37
+ readonly registrations: readonly ToolRegistration[];
38
+ readonly errorMapper?: McpErrorMapper;
39
+ }
40
+ /**
41
+ * Define an MCP server. Returns an {@link McpDefinition} that adapters
42
+ * (e.g. `@ganintegrity/mcp/express`'s `createMcpRouter`) materialise into
43
+ * a real SDK server per request.
44
+ *
45
+ * The `tools` callback runs once, here, at define time. The `register`
46
+ * argument captures each `register.tool(spec)` invocation; the wrapped
47
+ * handler bakes in ALS-reading, transaction lifecycle, and error mapping
48
+ * so per-request adapters can replay the registrations onto a fresh
49
+ * `McpServer` without any extra wiring.
50
+ */
51
+ export declare function defineMcpServer(options: DefineMcpServerOptions): McpDefinition;
52
+ /**
53
+ * Build a fresh SDK `McpServer` from an {@link McpDefinition} and replay
54
+ * every recorded tool registration onto it.
55
+ *
56
+ * Adapters call this once per HTTP request — the SDK's stateless
57
+ * Streamable-HTTP transport is single-use, which is why registrations
58
+ * are stored separately and replayed.
59
+ *
60
+ * Exposed for tests and advanced consumers writing custom adapters.
61
+ * Typical consumers don't call this directly.
62
+ */
63
+ export declare function materializeSdkServer(mcp: McpDefinition): McpServer;
64
+ //# sourceMappingURL=define.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/core/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG3E;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;CACvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,GAC9B,aAAa,CAcf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,CAiBlE"}
@@ -0,0 +1,48 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { wrapToolHandler } from "./tool/index.js";
3
+ /**
4
+ * Define an MCP server. Returns an {@link McpDefinition} that adapters
5
+ * (e.g. `@ganintegrity/mcp/express`'s `createMcpRouter`) materialise into
6
+ * a real SDK server per request.
7
+ *
8
+ * The `tools` callback runs once, here, at define time. The `register`
9
+ * argument captures each `register.tool(spec)` invocation; the wrapped
10
+ * handler bakes in ALS-reading, transaction lifecycle, and error mapping
11
+ * so per-request adapters can replay the registrations onto a fresh
12
+ * `McpServer` without any extra wiring.
13
+ */
14
+ export function defineMcpServer(options) {
15
+ const registrations = [];
16
+ const register = {
17
+ tool: (spec) => {
18
+ registrations.push(wrapToolHandler(spec));
19
+ },
20
+ };
21
+ options.tools(register);
22
+ return {
23
+ name: options.name,
24
+ version: options.version,
25
+ registrations,
26
+ errorMapper: options.errorMapper,
27
+ };
28
+ }
29
+ /**
30
+ * Build a fresh SDK `McpServer` from an {@link McpDefinition} and replay
31
+ * every recorded tool registration onto it.
32
+ *
33
+ * Adapters call this once per HTTP request — the SDK's stateless
34
+ * Streamable-HTTP transport is single-use, which is why registrations
35
+ * are stored separately and replayed.
36
+ *
37
+ * Exposed for tests and advanced consumers writing custom adapters.
38
+ * Typical consumers don't call this directly.
39
+ */
40
+ export function materializeSdkServer(mcp) {
41
+ const server = new McpServer({ name: mcp.name, version: mcp.version });
42
+ const register = server.registerTool.bind(server);
43
+ for (const reg of mcp.registrations) {
44
+ register(reg.name, reg.config, reg.handler);
45
+ }
46
+ return server;
47
+ }
48
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.js","sourceRoot":"","sources":["../../src/core/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAwClD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA+B;IAE/B,MAAM,aAAa,GAAuB,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAiB;QAC7B,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;YACb,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxB,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,aAAa;QACb,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAkB;IACrD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IASvE,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,IAAI,CACvC,MAAM,CACsB,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;QACpC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
+ import type { Postgan } from "@ganintegrity/postgan";
3
+ import type { Logger } from "pino";
4
+ import type { AuthUser } from "./auth/auth.types.ts";
5
+ import { type McpDefinition } from "./define.ts";
6
+ /**
7
+ * Per-request scope handed to {@link dispatchMcpRequest}. The framework
8
+ * adapter pulls these out of its native request shape (Express `req.user` /
9
+ * `req.postgan` / `req.mcpSessionId`; Koa `ctx.state.*`).
10
+ */
11
+ export interface DispatchScope {
12
+ user: AuthUser;
13
+ postgan: Postgan;
14
+ sessionId?: string;
15
+ }
16
+ /**
17
+ * Framework-neutral per-request dispatcher.
18
+ *
19
+ * Builds the {@link RequestStore} from `scope` + `mcp.errorMapper`,
20
+ * materialises a fresh SDK `McpServer` (with all tools replayed onto it),
21
+ * connects a fresh transport, runs `invokeTransport` inside
22
+ * `requestStore.run(...)` so tool handlers can pull request context from
23
+ * ALS, and tears server + transport down in `finally`. A failing close is
24
+ * logged at warn but never re-thrown.
25
+ *
26
+ * If `invokeTransport` throws, the error is logged at error level and
27
+ * `onTransportError` is called so the framework adapter can write a 500
28
+ * onto its native response object (with whatever headers-sent guard it
29
+ * uses). The error is otherwise swallowed — this is the request boundary.
30
+ *
31
+ * Never throws.
32
+ */
33
+ export declare function dispatchMcpRequest(mcp: McpDefinition, logger: Logger, scope: DispatchScope, invokeTransport: (transport: StreamableHTTPServerTransport) => Promise<void>, onTransportError: () => void): Promise<void>;
34
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/core/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,eAAe,EAAE,CAAC,SAAS,EAAE,6BAA6B,KAAK,OAAO,CAAC,IAAI,CAAC,EAC5E,gBAAgB,EAAE,MAAM,IAAI,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmCf"}