@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.
- package/README.md +186 -60
- package/dist/core/als.d.ts.map +1 -0
- package/dist/core/als.js.map +1 -0
- package/dist/core/auth/auth.types.d.ts.map +1 -0
- package/dist/{auth → core/auth}/auth.types.js.map +1 -1
- package/dist/core/auth/index.d.ts +33 -0
- package/dist/core/auth/index.d.ts.map +1 -0
- package/dist/core/auth/index.js +57 -0
- package/dist/core/auth/index.js.map +1 -0
- package/dist/core/define.d.ts +64 -0
- package/dist/core/define.d.ts.map +1 -0
- package/dist/core/define.js +48 -0
- package/dist/core/define.js.map +1 -0
- package/dist/core/dispatch.d.ts +34 -0
- package/dist/core/dispatch.d.ts.map +1 -0
- package/dist/core/dispatch.js +57 -0
- package/dist/core/dispatch.js.map +1 -0
- package/dist/core/errors/errors.types.d.ts.map +1 -0
- package/dist/core/errors/errors.types.js.map +1 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/test-helpers.d.ts +20 -0
- package/dist/core/test-helpers.d.ts.map +1 -0
- package/dist/core/test-helpers.js +38 -0
- package/dist/core/test-helpers.js.map +1 -0
- package/dist/core/tool/index.d.ts +19 -0
- package/dist/core/tool/index.d.ts.map +1 -0
- package/dist/{tool → core/tool}/index.js +28 -25
- package/dist/core/tool/index.js.map +1 -0
- package/dist/{tool → core/tool}/tool.types.d.ts +21 -0
- package/dist/core/tool/tool.types.d.ts.map +1 -0
- package/dist/{tool → core/tool}/tool.types.js.map +1 -1
- package/dist/{auth/index.d.ts → express/auth.d.ts} +4 -5
- package/dist/express/auth.d.ts.map +1 -0
- package/dist/express/auth.js +47 -0
- package/dist/express/auth.js.map +1 -0
- package/dist/express/express.types.d.ts +35 -0
- package/dist/express/express.types.d.ts.map +1 -0
- package/dist/express/express.types.js +2 -0
- package/dist/express/express.types.js.map +1 -0
- package/dist/express/index.d.ts +32 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +50 -0
- package/dist/express/index.js.map +1 -0
- package/dist/index.d.ts +13 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/als.d.ts.map +0 -1
- package/dist/als.js.map +0 -1
- package/dist/auth/auth.types.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -74
- package/dist/auth/index.js.map +0 -1
- package/dist/errors/errors.types.d.ts.map +0 -1
- package/dist/errors/errors.types.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/server/index.d.ts +0 -18
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -130
- package/dist/server/index.js.map +0 -1
- package/dist/server/server.types.d.ts +0 -78
- package/dist/server/server.types.d.ts.map +0 -1
- package/dist/server/server.types.js +0 -2
- package/dist/server/server.types.js.map +0 -1
- package/dist/tool/index.d.ts +0 -26
- package/dist/tool/index.d.ts.map +0 -1
- package/dist/tool/index.js.map +0 -1
- package/dist/tool/tool.types.d.ts.map +0 -1
- /package/dist/{als.d.ts → core/als.d.ts} +0 -0
- /package/dist/{als.js → core/als.js} +0 -0
- /package/dist/{auth → core/auth}/auth.types.d.ts +0 -0
- /package/dist/{auth → core/auth}/auth.types.js +0 -0
- /package/dist/{errors → core/errors}/errors.types.d.ts +0 -0
- /package/dist/{errors → core/errors}/errors.types.js +0 -0
- /package/dist/{errors → core/errors}/index.d.ts +0 -0
- /package/dist/{errors → core/errors}/index.js +0 -0
- /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
|
-
###
|
|
29
|
+
### Define-once, materialize-per-request
|
|
30
30
|
|
|
31
|
-
`
|
|
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
|
|
33
|
+
You write tool registrations once. The library handles the per-request lifecycle.
|
|
34
34
|
|
|
35
35
|
### The ALS scope
|
|
36
36
|
|
|
37
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
### `defineMcpServer(options)` — `@ganintegrity/mcp`
|
|
134
231
|
|
|
135
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
245
|
+
### `register.tool(spec)` — `@ganintegrity/mcp`
|
|
154
246
|
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
name: "
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
### `
|
|
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
|
-
|
|
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()`
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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":["
|
|
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"}
|