@kaminari-ad/mcp 0.5.2 → 0.7.2
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/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/bin.js +3 -5
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-3CY2WEZF.js → chunk-NRDA7DQQ.js} +92 -36
- package/dist/chunk-NRDA7DQQ.js.map +1 -0
- package/dist/{chunk-SQVFFWOB.js → chunk-VTKVXW4G.js} +3 -3
- package/dist/{chunk-SQVFFWOB.js.map → chunk-VTKVXW4G.js.map} +1 -1
- package/dist/{http-bootstrap-X2Y2VGBO.js → http-bootstrap-7VZLKAM4.js} +32 -159
- package/dist/http-bootstrap-7VZLKAM4.js.map +1 -0
- package/dist/{stdio-bootstrap-JBLNH46R.js → stdio-bootstrap-CE2Y76DX.js} +4 -4
- package/dist/{stdio-bootstrap-JBLNH46R.js.map → stdio-bootstrap-CE2Y76DX.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3CY2WEZF.js.map +0 -1
- package/dist/http-bootstrap-X2Y2VGBO.js.map +0 -1
|
@@ -3,8 +3,8 @@ export { err, ok } from 'neverthrow';
|
|
|
3
3
|
|
|
4
4
|
// src/shared/version.ts
|
|
5
5
|
var NAME = "@kaminari-ad/mcp";
|
|
6
|
-
var VERSION = "0.
|
|
6
|
+
var VERSION = "0.7.2";
|
|
7
7
|
|
|
8
8
|
export { NAME, VERSION };
|
|
9
|
-
//# sourceMappingURL=chunk-
|
|
10
|
-
//# sourceMappingURL=chunk-
|
|
9
|
+
//# sourceMappingURL=chunk-VTKVXW4G.js.map
|
|
10
|
+
//# sourceMappingURL=chunk-VTKVXW4G.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/version.ts"],"names":[],"mappings":";;;;AASO,IAAM,IAAA,GAAO;AACb,IAAM,OAAA,GAAU","file":"chunk-
|
|
1
|
+
{"version":3,"sources":["../src/shared/version.ts"],"names":[],"mappings":";;;;AASO,IAAM,IAAA,GAAO;AACb,IAAM,OAAA,GAAU","file":"chunk-VTKVXW4G.js","sourcesContent":["/**\n * Package version and name. Hard-coded as constants here, asserted to\n * match `package.json` by a unit test.\n *\n * Why not import `package.json`: it would force JSON-module support at\n * runtime and tsup-bundling would inline the entire manifest. Two\n * constants + one assertion test is simpler and gives the same safety.\n */\n\nexport const NAME = \"@kaminari-ad/mcp\";\nexport const VERSION = \"0.7.2\";\n"]}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-
|
|
3
|
-
import { VERSION, NAME } from './chunk-
|
|
2
|
+
import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-NRDA7DQQ.js';
|
|
3
|
+
import { VERSION, NAME } from './chunk-VTKVXW4G.js';
|
|
4
4
|
import { createServer } from 'http';
|
|
5
5
|
import process from 'process';
|
|
6
|
-
import { randomUUID } from 'crypto';
|
|
7
6
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
9
8
|
|
|
@@ -61,100 +60,28 @@ function createLeakyBucketRateLimiter(clock, rpm) {
|
|
|
61
60
|
}
|
|
62
61
|
};
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
// src/infrastructure/session/in-memory-session-store.ts
|
|
66
|
-
var SWEEP_EVERY_N_BINDS = 128;
|
|
67
|
-
function createInMemorySessionStore(clock, ttlMs) {
|
|
68
|
-
const entries = /* @__PURE__ */ new Map();
|
|
69
|
-
let bindCount = 0;
|
|
70
|
-
function isExpired(entry) {
|
|
71
|
-
return entry.expiresAtMs < clock.nowMs();
|
|
72
|
-
}
|
|
73
|
-
function sweepIfDue() {
|
|
74
|
-
bindCount += 1;
|
|
75
|
-
if (bindCount % SWEEP_EVERY_N_BINDS !== 0) return;
|
|
76
|
-
const now = clock.nowMs();
|
|
77
|
-
for (const [id, entry] of entries) {
|
|
78
|
-
if (entry.expiresAtMs < now) entries.delete(id);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
bind(sessionId, bearerHash) {
|
|
83
|
-
sweepIfDue();
|
|
84
|
-
const existing = entries.get(sessionId);
|
|
85
|
-
if (existing !== void 0 && !isExpired(existing)) {
|
|
86
|
-
if (existing.bearerHash !== bearerHash) {
|
|
87
|
-
return { kind: "bound-to-other-bearer" };
|
|
88
|
-
}
|
|
89
|
-
existing.expiresAtMs = clock.nowMs() + ttlMs;
|
|
90
|
-
return { kind: "ok" };
|
|
91
|
-
}
|
|
92
|
-
entries.set(sessionId, { bearerHash, expiresAtMs: clock.nowMs() + ttlMs });
|
|
93
|
-
return { kind: "ok" };
|
|
94
|
-
},
|
|
95
|
-
checkAndTouch(sessionId, bearerHash) {
|
|
96
|
-
const entry = entries.get(sessionId);
|
|
97
|
-
if (entry === void 0 || isExpired(entry)) {
|
|
98
|
-
if (entry !== void 0) entries.delete(sessionId);
|
|
99
|
-
return { kind: "unknown" };
|
|
100
|
-
}
|
|
101
|
-
if (entry.bearerHash !== bearerHash) {
|
|
102
|
-
return { kind: "bound-to-other-bearer" };
|
|
103
|
-
}
|
|
104
|
-
entry.expiresAtMs = clock.nowMs() + ttlMs;
|
|
105
|
-
return { kind: "ok" };
|
|
106
|
-
},
|
|
107
|
-
destroy(sessionId) {
|
|
108
|
-
entries.delete(sessionId);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
113
|
-
function parseSessionId(raw) {
|
|
114
|
-
return SESSION_ID_RE.test(raw) ? raw : void 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/presentation/http/mcp-session-factory.ts
|
|
118
|
-
async function initNewSession(args) {
|
|
119
|
-
const { reqLogger, liveSessions, sessions, bearer } = args;
|
|
63
|
+
async function createStatelessMcp(ctx) {
|
|
120
64
|
const server = new McpServer(
|
|
121
65
|
{ name: NAME, version: VERSION },
|
|
122
66
|
{ instructions: SERVER_INSTRUCTIONS }
|
|
123
67
|
);
|
|
124
|
-
|
|
125
|
-
const seedApi = new Proxy(seedTarget, {
|
|
126
|
-
get() {
|
|
127
|
-
throw new Error("HttpRequestHandler invariant violated: ctxRef.api not set for this request");
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
const ctxRef = {
|
|
131
|
-
current: { api: seedApi, logger: reqLogger, requestId: args.requestId }
|
|
132
|
-
};
|
|
133
|
-
wireToolsIntoMcpServer(server, () => ctxRef.current);
|
|
68
|
+
wireToolsIntoMcpServer(server, () => ctx);
|
|
134
69
|
declareEmptyResourcesAndPrompts(server);
|
|
135
70
|
const transport = new StreamableHTTPServerTransport({
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
sessions.bind(sid, bearer.fullHash());
|
|
142
|
-
liveSessions.set(sid, { server, transport, ctxRef });
|
|
143
|
-
},
|
|
144
|
-
onsessionclosed: (closed) => {
|
|
145
|
-
const sid = parseSessionId(closed);
|
|
146
|
-
if (sid === void 0) return;
|
|
147
|
-
sessions.destroy(sid);
|
|
148
|
-
liveSessions.delete(sid);
|
|
149
|
-
}
|
|
71
|
+
// Stateless mode: leaving `sessionIdGenerator` unset (undefined) tells
|
|
72
|
+
// the SDK transport to issue no Mcp-Session-Id and skip session
|
|
73
|
+
// validation. We omit the key rather than pass an explicit `undefined`
|
|
74
|
+
// because `exactOptionalPropertyTypes` rejects the latter.
|
|
75
|
+
enableJsonResponse: true
|
|
150
76
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
77
|
+
try {
|
|
78
|
+
await server.connect(transport);
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
await transport.close().catch(() => void 0);
|
|
81
|
+
await server.close().catch(() => void 0);
|
|
82
|
+
throw cause;
|
|
83
|
+
}
|
|
84
|
+
return { server, transport };
|
|
158
85
|
}
|
|
159
86
|
|
|
160
87
|
// src/presentation/http/protected-resource-metadata-handler.ts
|
|
@@ -175,50 +102,6 @@ function respondWithProtectedResourceMetadata(res, config) {
|
|
|
175
102
|
res.end(JSON.stringify(body));
|
|
176
103
|
}
|
|
177
104
|
|
|
178
|
-
// src/domain/services/session-binding-policy.ts
|
|
179
|
-
function decideSessionAction(check) {
|
|
180
|
-
switch (check.kind) {
|
|
181
|
-
case "ok":
|
|
182
|
-
return { kind: "allow" };
|
|
183
|
-
case "unknown":
|
|
184
|
-
return { kind: "unknown-session" };
|
|
185
|
-
case "bound-to-other-bearer":
|
|
186
|
-
return { kind: "reject-bearer-mismatch" };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/presentation/http/session-resolver.ts
|
|
191
|
-
async function resolveExistingSession(args) {
|
|
192
|
-
const { sessionIdRaw, bearer, reqLogger, sessions, liveSessions, res } = args;
|
|
193
|
-
if (sessionIdRaw === void 0) return void 0;
|
|
194
|
-
const sessionId = parseSessionId(sessionIdRaw);
|
|
195
|
-
if (sessionId === void 0) {
|
|
196
|
-
writeJson(res, 400, { error: "Invalid Mcp-Session-Id" });
|
|
197
|
-
return "rejected";
|
|
198
|
-
}
|
|
199
|
-
const action = decideSessionAction(sessions.checkAndTouch(sessionId, bearer.fullHash()));
|
|
200
|
-
if (action.kind === "reject-bearer-mismatch") {
|
|
201
|
-
sessions.destroy(sessionId);
|
|
202
|
-
const evicted = liveSessions.get(sessionId);
|
|
203
|
-
liveSessions.delete(sessionId);
|
|
204
|
-
if (evicted !== void 0) await evicted.transport.close().catch(() => void 0);
|
|
205
|
-
reqLogger.warn({}, "http.session_bearer_mismatch");
|
|
206
|
-
writeJson(res, 401, { error: "Session bound to a different bearer" });
|
|
207
|
-
return "rejected";
|
|
208
|
-
}
|
|
209
|
-
const cached = liveSessions.get(sessionId);
|
|
210
|
-
if (cached !== void 0 && action.kind === "allow") return cached;
|
|
211
|
-
if (cached !== void 0) {
|
|
212
|
-
liveSessions.delete(sessionId);
|
|
213
|
-
await cached.transport.close().catch(() => void 0);
|
|
214
|
-
}
|
|
215
|
-
return void 0;
|
|
216
|
-
}
|
|
217
|
-
function writeJson(res, status, body) {
|
|
218
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
219
|
-
res.end(JSON.stringify(body));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
105
|
// src/presentation/http/www-authenticate.ts
|
|
223
106
|
function buildBearerChallenge(config) {
|
|
224
107
|
const resourceMetadata = `resource_metadata="${config.oauthProtectedResourceMetadataUrl}"`;
|
|
@@ -229,12 +112,11 @@ function buildBearerChallenge(config) {
|
|
|
229
112
|
|
|
230
113
|
// src/presentation/http/http-request-handler.ts
|
|
231
114
|
function createHttpRequestHandler(deps) {
|
|
232
|
-
const { config, logger,
|
|
233
|
-
const liveSessions = /* @__PURE__ */ new Map();
|
|
115
|
+
const { config, logger, rateLimiter } = deps;
|
|
234
116
|
const bearerChallenge = buildBearerChallenge(config);
|
|
235
117
|
async function handle(req, res) {
|
|
236
118
|
if (req.method === "GET" && req.url === "/healthz") {
|
|
237
|
-
|
|
119
|
+
writeJson(res, 200, { status: "ok" });
|
|
238
120
|
return;
|
|
239
121
|
}
|
|
240
122
|
if (req.method === "GET" && req.url === "/.well-known/oauth-protected-resource") {
|
|
@@ -242,13 +124,13 @@ function createHttpRequestHandler(deps) {
|
|
|
242
124
|
return;
|
|
243
125
|
}
|
|
244
126
|
if (req.url !== "/mcp" || req.method !== "POST" && req.method !== "GET" && req.method !== "DELETE") {
|
|
245
|
-
|
|
127
|
+
writeJson(res, 404, { error: "Not found" });
|
|
246
128
|
return;
|
|
247
129
|
}
|
|
248
130
|
const authHeader = first(req.headers.authorization);
|
|
249
131
|
const bearer = BearerToken.fromAuthorizationHeader(authHeader);
|
|
250
132
|
if (bearer === void 0) {
|
|
251
|
-
|
|
133
|
+
writeJson(
|
|
252
134
|
res,
|
|
253
135
|
401,
|
|
254
136
|
{ error: "Authorization Bearer token required" },
|
|
@@ -266,29 +148,18 @@ function createHttpRequestHandler(deps) {
|
|
|
266
148
|
"http.rate_limited"
|
|
267
149
|
);
|
|
268
150
|
const headers = rate.retryAfterMs !== void 0 ? { "retry-after": String(Math.ceil(rate.retryAfterMs / 1e3)) } : {};
|
|
269
|
-
|
|
151
|
+
writeJson(res, 429, { error: "Rate limited" }, headers);
|
|
270
152
|
return;
|
|
271
153
|
}
|
|
272
|
-
const sessionIdRaw = first(req.headers["mcp-session-id"]);
|
|
273
|
-
const existingEntry = await resolveExistingSession({
|
|
274
|
-
sessionIdRaw,
|
|
275
|
-
bearer,
|
|
276
|
-
reqLogger,
|
|
277
|
-
sessions,
|
|
278
|
-
liveSessions,
|
|
279
|
-
res
|
|
280
|
-
});
|
|
281
|
-
if (existingEntry === "rejected") return;
|
|
282
154
|
const api = createHttpApiGateway({
|
|
283
155
|
baseUrl: config.apiBaseUrl,
|
|
284
156
|
bearer,
|
|
285
157
|
requestId,
|
|
286
158
|
logger: reqLogger
|
|
287
159
|
});
|
|
288
|
-
const
|
|
289
|
-
entry.ctxRef.current = { api, logger: reqLogger, requestId };
|
|
160
|
+
const { server, transport } = await createStatelessMcp({ api, logger: reqLogger, requestId });
|
|
290
161
|
try {
|
|
291
|
-
await
|
|
162
|
+
await transport.handleRequest(req, res);
|
|
292
163
|
reqLogger.info({}, "http.request_done");
|
|
293
164
|
} catch (cause) {
|
|
294
165
|
reqLogger.error(
|
|
@@ -296,13 +167,16 @@ function createHttpRequestHandler(deps) {
|
|
|
296
167
|
"http.handler_error"
|
|
297
168
|
);
|
|
298
169
|
if (!res.headersSent) {
|
|
299
|
-
|
|
170
|
+
writeJson(res, 500, { error: "Internal server error" });
|
|
300
171
|
}
|
|
172
|
+
} finally {
|
|
173
|
+
void transport.close().catch(() => void 0);
|
|
174
|
+
void server.close().catch(() => void 0);
|
|
301
175
|
}
|
|
302
176
|
}
|
|
303
177
|
return handle;
|
|
304
178
|
}
|
|
305
|
-
function
|
|
179
|
+
function writeJson(res, status, body, extraHeaders = {}) {
|
|
306
180
|
res.writeHead(status, { "content-type": "application/json", ...extraHeaders });
|
|
307
181
|
res.end(JSON.stringify(body));
|
|
308
182
|
}
|
|
@@ -321,9 +195,8 @@ async function bootstrapHttp(config) {
|
|
|
321
195
|
return 2;
|
|
322
196
|
}
|
|
323
197
|
const clock = createSystemClock();
|
|
324
|
-
const sessions = createInMemorySessionStore(clock, config.sessionTtlSec * 1e3);
|
|
325
198
|
const rateLimiter = createLeakyBucketRateLimiter(clock, config.rateLimitRpm);
|
|
326
|
-
const handle = createHttpRequestHandler({ config, logger,
|
|
199
|
+
const handle = createHttpRequestHandler({ config, logger, rateLimiter });
|
|
327
200
|
const httpServer = createServer((req, res) => {
|
|
328
201
|
res.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload");
|
|
329
202
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -361,5 +234,5 @@ async function bootstrapHttp(config) {
|
|
|
361
234
|
}
|
|
362
235
|
|
|
363
236
|
export { bootstrapHttp };
|
|
364
|
-
//# sourceMappingURL=http-bootstrap-
|
|
365
|
-
//# sourceMappingURL=http-bootstrap-
|
|
237
|
+
//# sourceMappingURL=http-bootstrap-7VZLKAM4.js.map
|
|
238
|
+
//# sourceMappingURL=http-bootstrap-7VZLKAM4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/infrastructure/clock/system-clock.ts","../src/infrastructure/rate-limit/leaky-bucket-rate-limiter.ts","../src/presentation/http/create-stateless-mcp.ts","../src/presentation/http/protected-resource-metadata-handler.ts","../src/presentation/http/www-authenticate.ts","../src/presentation/http/http-request-handler.ts","../src/presentation/http/http-bootstrap.ts"],"names":[],"mappings":"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","file":"http-bootstrap-7VZLKAM4.js","sourcesContent":["/**\n * Production {@link Clock} adapter. Reads the system clock via\n * `Date.now()`. Tests use `FakeClock` instead.\n */\n\nimport type { Clock } from \"../../domain/ports/clock.js\";\n\n/**\n * Returns a fresh `Clock` backed by `Date.now()`.\n */\nexport function createSystemClock(): Clock {\n return {\n nowMs(): number {\n return Date.now();\n },\n };\n}\n","/**\n * Per-tenant leaky-bucket rate limiter.\n *\n * Tenant key = `bearerHash` (NOT the raw bearer). Each bucket has a\n * capacity of `rpm` tokens and refills `rpm / 60` tokens per second.\n *\n * The store is in-memory per process: it does NOT coordinate across\n * replicas. A multi-replica deployment would multiply the effective\n * limit by N. This is acceptable as a first-line defense against\n * runaway agent loops and brute-force token scans; the API has its\n * own rate limit as the authoritative gate.\n *\n * Memory: we sweep the bucket map every {@link SWEEP_EVERY_N_CHECKS}\n * `check()` calls and evict any bucket that hasn't been touched in\n * {@link SWEEP_IDLE_MS}. After that long an idle period the bucket\n * is fully refilled (refill rate is `rpm / 60_000` ms, so anything\n * past the refill window is at capacity), and dropping a full bucket\n * is equivalent to a fresh allocation on the next request — no\n * loss of rate-limit fidelity. Prevents unbounded growth from\n * one-shot bearers / rotated API keys.\n *\n * The bucket map only holds hashes — no tenant data ever leaks here.\n */\n\nimport type { Clock } from \"../../domain/ports/clock.js\";\nimport type { RateLimitDecision, RateLimiter } from \"../../domain/ports/rate-limiter.js\";\n\ninterface Bucket {\n /** Available tokens (fractional, refilled over time). */\n tokens: number;\n /** When tokens were last refilled (epoch ms). */\n lastRefillMs: number;\n}\n\n/**\n * How often (in `check()` calls) the limiter walks its bucket map to\n * evict full-and-idle buckets. Tuned so a single-request burst of N\n * distinct bearers cannot grow the map past ~`2 * SWEEP_EVERY_N_CHECKS`.\n */\nconst SWEEP_EVERY_N_CHECKS = 256;\n/**\n * Bucket must be idle for at least this long since the last `check`\n * to be sweep-eligible. Equal to the refill window (60s) so the\n * bucket is guaranteed to be at full capacity when we drop it —\n * eviction has no observable effect on the next caller.\n */\nconst SWEEP_IDLE_MS = 60_000;\n\n/**\n * Build a leaky-bucket rate limiter.\n *\n * @param clock - Clock for token refill calculations.\n * @param rpm - Requests-per-minute capacity per tenant hash.\n */\nexport function createLeakyBucketRateLimiter(clock: Clock, rpm: number): RateLimiter {\n if (rpm < 1) throw new Error(\"rpm must be >= 1\");\n const refillPerMs = rpm / 60_000;\n const buckets = new Map<string, Bucket>();\n let checkCount = 0;\n\n function refill(bucket: Bucket): void {\n const now = clock.nowMs();\n const elapsed = now - bucket.lastRefillMs;\n if (elapsed > 0) {\n bucket.tokens = Math.min(rpm, bucket.tokens + elapsed * refillPerMs);\n bucket.lastRefillMs = now;\n }\n }\n\n function sweepIfDue(): void {\n checkCount += 1;\n if (checkCount % SWEEP_EVERY_N_CHECKS !== 0) return;\n const now = clock.nowMs();\n for (const [hash, bucket] of buckets) {\n if (now - bucket.lastRefillMs >= SWEEP_IDLE_MS) {\n buckets.delete(hash);\n }\n }\n }\n\n return {\n check(tenantHash: string): RateLimitDecision {\n sweepIfDue();\n let bucket = buckets.get(tenantHash);\n if (bucket === undefined) {\n bucket = { tokens: rpm, lastRefillMs: clock.nowMs() };\n buckets.set(tenantHash, bucket);\n }\n refill(bucket);\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1;\n return { allowed: true };\n }\n const deficit = 1 - bucket.tokens;\n const retryAfterMs = Math.ceil(deficit / refillPerMs);\n return { allowed: false, retryAfterMs };\n },\n };\n}\n","/**\n * Build a fresh, single-use MCP server + transport for ONE stateless\n * HTTP request.\n *\n * Stateless mode (`sessionIdGenerator: undefined`): the SDK issues no\n * `Mcp-Session-Id` and performs no session validation, so any replica\n * can serve any request — no sticky routing, no shared session store.\n * The SDK requires a fresh transport per request (reusing a stateless\n * transport collides message ids across clients), so the caller MUST\n * close both `server` and `transport` once the response is sent.\n *\n * Every request is independently authenticated by its own Bearer via the\n * `ToolContext.api` gateway the caller builds; there is no cross-request\n * state to hijack (see CONTRIBUTING.md \"Tenant isolation\").\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\n\nimport type { ToolContext } from \"../../application/tools/_shared/tool-context.js\";\nimport { SERVER_INSTRUCTIONS } from \"../../shared/server-instructions.js\";\nimport { NAME, VERSION } from \"../../shared/version.js\";\nimport { declareEmptyResourcesAndPrompts } from \"../shared/declare-empty-caps.js\";\nimport { wireToolsIntoMcpServer } from \"../shared/wire-tools.js\";\n\n/**\n * A single-use MCP server + transport pair for one stateless request.\n * The caller closes both after the response (see the handler).\n */\nexport interface StatelessMcp {\n readonly server: McpServer;\n readonly transport: StreamableHTTPServerTransport;\n}\n\n/**\n * Create a fresh stateless MCP server + transport bound to one request's\n * {@link ToolContext}. No session id is issued; the transport handles a\n * single request and is then discarded by the caller.\n */\nexport async function createStatelessMcp(ctx: ToolContext): Promise<StatelessMcp> {\n const server = new McpServer(\n { name: NAME, version: VERSION },\n { instructions: SERVER_INSTRUCTIONS }\n );\n // The ctx is fixed for this request, so a constant provider is correct\n // (no per-request swap needed — the server is single-use).\n wireToolsIntoMcpServer(server, () => ctx);\n declareEmptyResourcesAndPrompts(server);\n\n const transport = new StreamableHTTPServerTransport({\n // Stateless mode: leaving `sessionIdGenerator` unset (undefined) tells\n // the SDK transport to issue no Mcp-Session-Id and skip session\n // validation. We omit the key rather than pass an explicit `undefined`\n // because `exactOptionalPropertyTypes` rejects the latter.\n enableJsonResponse: true,\n });\n\n try {\n // @ts-expect-error SDK's Transport.onclose union (() => void) | undefined\n // mismatches Server.connect's expected non-optional type. Harmless;\n // fixed upstream in a future SDK release.\n await server.connect(transport);\n } catch (cause) {\n // Connect failed — free both so a failed request can't leak the\n // half-built pair (the handler only registers cleanup on success).\n await transport.close().catch(() => undefined);\n await server.close().catch(() => undefined);\n throw cause;\n }\n return { server, transport };\n}\n","/**\n * Handler for `GET /.well-known/oauth-protected-resource` — the\n * RFC 9728 protected-resource metadata document.\n *\n * Anthropic's Claude directory clients fetch this document to discover\n * (a) the Authorization Server that issues tokens for this resource\n * and (b) the canonical resource identifier they should pass as the\n * `aud` claim / `resource` parameter. The document is **public,\n * static, contains no tenant data**, and is served without any\n * Authorization check — the same pattern as `/healthz` per\n * `mcp-tenant-isolation.mdc` rule §16.\n *\n * Body shape is locked by a golden-file test (`tests/isolation/\n * oauth-discovery.test.ts`) so a refactor cannot silently break Claude\n * discovery — the spec's `2025-11-25` revision is the contract.\n */\n\nimport type { ServerResponse } from \"node:http\";\n\nimport type { Config } from \"../../shared/config.js\";\n\n/**\n * Build the JSON body of the protected-resource metadata document.\n *\n * Pure function — no `ServerResponse` dependency — so the document\n * shape can be byte-stably asserted in unit tests without spinning\n * up a server.\n */\nexport function buildProtectedResourceMetadata(config: Config): Readonly<Record<string, unknown>> {\n return {\n resource: config.oauthProtectedResource,\n authorization_servers: [config.oauthAuthorizationServerUrl],\n scopes_supported: [...config.oauthScopes],\n bearer_methods_supported: [\"header\"],\n };\n}\n\n/**\n * Write the metadata document as a `200 OK` JSON response. Sets\n * `Cache-Control: public, max-age=3600` so well-behaved clients\n * (Anthropic edges, CDNs) don't hammer the endpoint, but keep the\n * staleness window short so a scope-catalogue update propagates\n * within an hour.\n */\nexport function respondWithProtectedResourceMetadata(res: ServerResponse, config: Config): void {\n const body = buildProtectedResourceMetadata(config);\n res.writeHead(200, {\n \"content-type\": \"application/json\",\n \"cache-control\": \"public, max-age=3600\",\n });\n res.end(JSON.stringify(body));\n}\n","/**\n * Pure builder for the `WWW-Authenticate: Bearer …` challenge that the\n * Resource Server returns on 401 responses.\n *\n * Follows RFC 6750 §3 for the `Bearer` scheme and the MCP authorization\n * spec's `resource_metadata` extension (advertised in the Anthropic\n * Claude directory docs) which points clients at our RFC 9728\n * protected-resource metadata document.\n *\n * Kept in its own file so callers don't drag the formatting concerns\n * into request-handler hot paths, and so `http-request-handler.ts`\n * stays under the 200-line effective limit (see `mcp-clean-code.mdc`\n * → File Size).\n */\n\nimport type { Config } from \"../../shared/config.js\";\n\n/**\n * Build the value of the `WWW-Authenticate` header for an unauthorized\n * MCP request. Includes the metadata-URL pointer Claude needs to\n * discover our Authorization Server, plus the space-delimited list of\n * scopes the operator has configured.\n *\n * Empty `oauthScopes` (an operator misconfiguration) is rendered as\n * an empty `scope=\"\"` parameter — still RFC-shaped, so downstream\n * parsers don't crash, and the misconfiguration surfaces in client\n * logs rather than silently dropping the scope hint.\n */\nexport function buildBearerChallenge(config: Config): string {\n const resourceMetadata = `resource_metadata=\"${config.oauthProtectedResourceMetadataUrl}\"`;\n const scopeValue = config.oauthScopes.join(\" \");\n const scope = `scope=\"${scopeValue}\"`;\n return `Bearer ${resourceMetadata}, ${scope}`;\n}\n","/**\n * Per-request HTTP handler for the hosted MCP endpoint.\n *\n * This module is the single place that enforces the tenant-isolation\n * rules from CONTRIBUTING.md. Each rule reference (#N) maps to the\n * corresponding numbered entry there.\n *\n * The function returned by {@link createHttpRequestHandler} is pure\n * over its dependencies — no module-level state, no shared mutable\n * caches across handler factories. Long-lived dependencies\n * (`RateLimiter`, top-level `Logger`) are supplied by the caller\n * (`http-bootstrap.ts`); each request constructs a fresh per-request\n * `ApiGateway` from the incoming Bearer.\n *\n * Stateless transport: every request builds a single-use MCP\n * server + transport (no `Mcp-Session-Id`, no session validation, no\n * cross-request state), handles exactly one request, and closes both.\n * Any replica can serve any request — no sticky routing. Each request\n * is independently authenticated by its own Bearer, so there is no\n * session to hijack (rule #7).\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nimport type { Logger } from \"../../domain/ports/logger.js\";\nimport type { RateLimiter } from \"../../domain/ports/rate-limiter.js\";\nimport { BearerToken } from \"../../domain/value-objects/bearer-token.js\";\nimport { newRequestId } from \"../../domain/value-objects/request-id.js\";\nimport { createHttpApiGateway } from \"../../infrastructure/api/http-api-gateway.js\";\nimport type { Config } from \"../../shared/config.js\";\nimport { createStatelessMcp } from \"./create-stateless-mcp.js\";\nimport { respondWithProtectedResourceMetadata } from \"./protected-resource-metadata-handler.js\";\nimport { buildBearerChallenge } from \"./www-authenticate.js\";\n\nexport interface HttpRequestHandlerDeps {\n readonly config: Config;\n readonly logger: Logger;\n readonly rateLimiter: RateLimiter;\n}\n\n/**\n * Build the per-request handler. Returns an async function with the\n * Node `http.Server` request-handler signature.\n */\nexport function createHttpRequestHandler(\n deps: HttpRequestHandlerDeps\n): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n const { config, logger, rateLimiter } = deps;\n\n // Pre-computed per process: the WWW-Authenticate challenge string is\n // purely a function of `Config` (resource-metadata URL + scopes).\n // Computing it once avoids string-building on every unauthenticated\n // request (rule #1 — a const closure, not module-level mutable state).\n const bearerChallenge = buildBearerChallenge(config);\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // Rule #16 — health probe carries no tenant data and needs no auth.\n if (req.method === \"GET\" && req.url === \"/healthz\") {\n writeJson(res, 200, { status: \"ok\" });\n return;\n }\n\n // RFC 9728 protected-resource metadata. Same data-free, no-auth\n // pattern as /healthz — see protected-resource-metadata-handler.ts.\n if (req.method === \"GET\" && req.url === \"/.well-known/oauth-protected-resource\") {\n respondWithProtectedResourceMetadata(res, config);\n return;\n }\n\n if (\n req.url !== \"/mcp\" ||\n (req.method !== \"POST\" && req.method !== \"GET\" && req.method !== \"DELETE\")\n ) {\n writeJson(res, 404, { error: \"Not found\" });\n return;\n }\n\n // Rule #6 — missing Authorization is rejected without touching the\n // API. The `WWW-Authenticate` header is required by the MCP\n // authorization spec / Anthropic Claude clients to discover the\n // RFC 9728 protected-resource metadata document.\n const authHeader = first(req.headers.authorization);\n const bearer = BearerToken.fromAuthorizationHeader(authHeader);\n if (bearer === undefined) {\n writeJson(\n res,\n 401,\n { error: \"Authorization Bearer token required\" },\n { \"www-authenticate\": bearerChallenge }\n );\n return;\n }\n\n const requestId = newRequestId();\n const bearerHash = bearer.hash();\n const reqLogger = logger.child({ request_id: requestId, bearer_hash: bearerHash });\n\n // Rule #14 — per-bearer rate limit, checked before any API work.\n const rate = rateLimiter.check(bearer.fullHash());\n if (!rate.allowed) {\n reqLogger.warn(\n { rate_limited: true, retry_after_ms: rate.retryAfterMs ?? 0 },\n \"http.rate_limited\"\n );\n const headers =\n rate.retryAfterMs !== undefined\n ? { \"retry-after\": String(Math.ceil(rate.retryAfterMs / 1000)) }\n : {};\n writeJson(res, 429, { error: \"Rate limited\" }, headers);\n return;\n }\n\n // Rule #3 — per-request ApiGateway, holding only this request's Bearer.\n const api = createHttpApiGateway({\n baseUrl: config.apiBaseUrl,\n bearer,\n requestId,\n logger: reqLogger,\n });\n\n // Single-use stateless MCP for this request. Closed deterministically\n // in `finally` once the response is written, so the per-request SDK\n // objects don't accumulate across keep-alive requests (the SDK forbids\n // reusing a stateless transport anyway).\n const { server, transport } = await createStatelessMcp({ api, logger: reqLogger, requestId });\n try {\n await transport.handleRequest(req, res);\n reqLogger.info({}, \"http.request_done\");\n } catch (cause) {\n reqLogger.error(\n { error_message: cause instanceof Error ? cause.message : \"unknown\" },\n \"http.handler_error\"\n );\n if (!res.headersSent) {\n writeJson(res, 500, { error: \"Internal server error\" });\n }\n } finally {\n void transport.close().catch(() => undefined);\n void server.close().catch(() => undefined);\n }\n }\n\n return handle;\n}\n\nfunction writeJson(\n res: ServerResponse,\n status: number,\n body: unknown,\n extraHeaders: Readonly<Record<string, string>> = {}\n): void {\n res.writeHead(status, { \"content-type\": \"application/json\", ...extraHeaders });\n res.end(JSON.stringify(body));\n}\n\nfunction first(headerValue: string | string[] | number | undefined): string | undefined {\n if (headerValue === undefined) return undefined;\n if (Array.isArray(headerValue)) return headerValue[0];\n if (typeof headerValue === \"number\") return String(headerValue);\n return headerValue;\n}\n","/**\n * Composition root for the HTTP transport.\n *\n * Wires the long-lived per-process dependencies (logger, rate limiter)\n * and hands the per-request handling off to\n * {@link createHttpRequestHandler} — which enforces every\n * tenant-isolation rule from CONTRIBUTING.md.\n *\n * Invariants enforced by this bootstrap itself:\n *\n * - Rule #5: `KAMINARI_AD_API_KEY` env var is REJECTED in HTTP mode.\n * If present, the process exits non-zero on startup so it can never\n * accidentally serve a default-Bearer fallback.\n * - Rule #1: every binding here is `const`. No module-level mutables.\n * - Rule #15: no telemetry SDK wired by default.\n */\n\nimport { createServer } from \"node:http\";\nimport process from \"node:process\";\n\nimport { createSystemClock } from \"../../infrastructure/clock/system-clock.js\";\nimport { createPinoLogger } from \"../../infrastructure/logging/pino-logger.js\";\nimport { createLeakyBucketRateLimiter } from \"../../infrastructure/rate-limit/leaky-bucket-rate-limiter.js\";\nimport type { Config } from \"../../shared/config.js\";\nimport { createHttpRequestHandler } from \"./http-request-handler.js\";\n\n/**\n * Build and start the HTTP MCP server. Resolves with a process exit\n * code when the server shuts down (SIGTERM / SIGINT).\n */\nexport async function bootstrapHttp(config: Config): Promise<number> {\n const logger = createPinoLogger(config.logLevel, config.logFormat);\n\n // Rule #5 — KAMINARI_AD_API_KEY is stdio-only. Refuse to start with\n // it set in HTTP mode so we never serve a default-Bearer fallback.\n if (config.stdioApiKey !== undefined) {\n logger.fatal({}, \"http.api_key_env_forbidden\");\n return 2;\n }\n\n const clock = createSystemClock();\n const rateLimiter = createLeakyBucketRateLimiter(clock, config.rateLimitRpm);\n\n const handle = createHttpRequestHandler({ config, logger, rateLimiter });\n\n const httpServer = createServer((req, res) => {\n // Security headers (moved from the rnd edge so mcp.kaminari.ad is\n // self-contained). Set on every response before handling; they persist\n // through the handler's writeHead (distinct keys). No CORS / no\n // org-identifying headers per CONTRIBUTING.md rules #8 / #12.\n res.setHeader(\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\");\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n res.setHeader(\"X-Frame-Options\", \"DENY\");\n res.setHeader(\"Referrer-Policy\", \"no-referrer\");\n res.setHeader(\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=()\");\n handle(req, res).catch((cause: unknown) => {\n logger.error(\n { error_message: cause instanceof Error ? cause.message : String(cause) },\n \"http.unhandled\"\n );\n if (!res.headersSent) {\n res.writeHead(500, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal server error\" }));\n }\n });\n });\n\n await new Promise<void>((resolve) => {\n httpServer.listen(config.httpPort, () => {\n logger.info({ http_port: config.httpPort }, \"http.ready\");\n resolve();\n });\n });\n\n await new Promise<void>((resolve) => {\n const onSignal = (): void => {\n logger.info({}, \"http.shutdown\");\n httpServer.close(() => {\n resolve();\n });\n };\n process.once(\"SIGTERM\", onSignal);\n process.once(\"SIGINT\", onSignal);\n });\n return 0;\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-
|
|
3
|
-
import { VERSION, NAME } from './chunk-
|
|
2
|
+
import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-NRDA7DQQ.js';
|
|
3
|
+
import { VERSION, NAME } from './chunk-VTKVXW4G.js';
|
|
4
4
|
import process from 'process';
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -47,5 +47,5 @@ async function bootstrapStdio(config) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export { bootstrapStdio };
|
|
50
|
-
//# sourceMappingURL=stdio-bootstrap-
|
|
51
|
-
//# sourceMappingURL=stdio-bootstrap-
|
|
50
|
+
//# sourceMappingURL=stdio-bootstrap-CE2Y76DX.js.map
|
|
51
|
+
//# sourceMappingURL=stdio-bootstrap-CE2Y76DX.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/presentation/stdio/stdio-bootstrap.ts"],"names":[],"mappings":";;;;;;;AAgCA,eAAsB,eAAe,MAAA,EAAiC;AACpE,EAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,MAAA,CAAO,QAAA,EAAU,OAAO,SAAS,CAAA;AAEjE,EAAA,IAAI,MAAA,CAAO,gBAAgB,MAAA,EAAW;AACpC,IAAA,MAAA,CAAO,KAAA,CAAM,EAAC,EAAG,uBAAuB,CAAA;AACxC,IAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,MACb;AAAA,KACF;AACA,IAAA,OAAO,CAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,UAAA,CAAW,MAAA,CAAO,WAAW,CAAA;AACxD,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,MAAA,CAAO,KAAA,CAAM,EAAC,EAAG,uBAAuB,CAAA;AACxC,IAAA,OAAO,CAAA;AAAA,EACT;AAEA,EAAA,MAAM,YAAY,YAAA,EAAa;AAC/B,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,KAAA,CAAM,EAAE,UAAA,EAAY,WAAW,WAAA,EAAa,MAAA,CAAO,IAAA,EAAK,EAAG,CAAA;AACvF,EAAA,MAAM,MAAM,oBAAA,CAAqB;AAAA,IAC/B,SAAS,MAAA,CAAO,UAAA;AAAA,IAChB,MAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA,EAAQ;AAAA,GACT,CAAA;AAED,EAAA,MAAM,GAAA,GAAM,EAAE,GAAA,EAAK,MAAA,EAAQ,cAAc,SAAA,EAAU;AACnD,EAAA,MAAM,SAAS,IAAI,SAAA;AAAA,IACjB,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,OAAA,EAAQ;AAAA,IAC/B,EAAE,cAAc,mBAAA;AAAoB,GACtC;AACA,EAAA,sBAAA,CAAuB,MAAA,EAAQ,MAAM,GAAG,CAAA;AACxC,EAAA,+BAAA,CAAgC,MAAM,CAAA;AAEtC,EAAA,MAAM,SAAA,GAAY,IAAI,oBAAA,EAAqB;AAC3C,EAAA,MAAM,MAAA,CAAO,QAAQ,SAAS,CAAA;AAC9B,EAAA,YAAA,CAAa,IAAA,CAAK,EAAC,EAAG,aAAa,CAAA;AAEnC,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,IAAA,SAAA,CAAU,UAAU,MAAY;AAC9B,MAAA,OAAA,EAAQ;AAAA,IACV,CAAA;AAAA,EACF,CAAC,CAAA;AACD,EAAA,YAAA,CAAa,IAAA,CAAK,EAAC,EAAG,gBAAgB,CAAA;AACtC,EAAA,OAAO,CAAA;AACT","file":"stdio-bootstrap-
|
|
1
|
+
{"version":3,"sources":["../src/presentation/stdio/stdio-bootstrap.ts"],"names":[],"mappings":";;;;;;;AAgCA,eAAsB,eAAe,MAAA,EAAiC;AACpE,EAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,MAAA,CAAO,QAAA,EAAU,OAAO,SAAS,CAAA;AAEjE,EAAA,IAAI,MAAA,CAAO,gBAAgB,MAAA,EAAW;AACpC,IAAA,MAAA,CAAO,KAAA,CAAM,EAAC,EAAG,uBAAuB,CAAA;AACxC,IAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,MACb;AAAA,KACF;AACA,IAAA,OAAO,CAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,UAAA,CAAW,MAAA,CAAO,WAAW,CAAA;AACxD,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,MAAA,CAAO,KAAA,CAAM,EAAC,EAAG,uBAAuB,CAAA;AACxC,IAAA,OAAO,CAAA;AAAA,EACT;AAEA,EAAA,MAAM,YAAY,YAAA,EAAa;AAC/B,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,KAAA,CAAM,EAAE,UAAA,EAAY,WAAW,WAAA,EAAa,MAAA,CAAO,IAAA,EAAK,EAAG,CAAA;AACvF,EAAA,MAAM,MAAM,oBAAA,CAAqB;AAAA,IAC/B,SAAS,MAAA,CAAO,UAAA;AAAA,IAChB,MAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA,EAAQ;AAAA,GACT,CAAA;AAED,EAAA,MAAM,GAAA,GAAM,EAAE,GAAA,EAAK,MAAA,EAAQ,cAAc,SAAA,EAAU;AACnD,EAAA,MAAM,SAAS,IAAI,SAAA;AAAA,IACjB,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,OAAA,EAAQ;AAAA,IAC/B,EAAE,cAAc,mBAAA;AAAoB,GACtC;AACA,EAAA,sBAAA,CAAuB,MAAA,EAAQ,MAAM,GAAG,CAAA;AACxC,EAAA,+BAAA,CAAgC,MAAM,CAAA;AAEtC,EAAA,MAAM,SAAA,GAAY,IAAI,oBAAA,EAAqB;AAC3C,EAAA,MAAM,MAAA,CAAO,QAAQ,SAAS,CAAA;AAC9B,EAAA,YAAA,CAAa,IAAA,CAAK,EAAC,EAAG,aAAa,CAAA;AAEnC,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,IAAA,SAAA,CAAU,UAAU,MAAY;AAC9B,MAAA,OAAA,EAAQ;AAAA,IACV,CAAA;AAAA,EACF,CAAC,CAAA;AACD,EAAA,YAAA,CAAa,IAAA,CAAK,EAAC,EAAG,gBAAgB,CAAA;AACtC,EAAA,OAAO,CAAA;AACT","file":"stdio-bootstrap-CE2Y76DX.js","sourcesContent":["/**\n * Composition root for the stdio transport.\n *\n * One process = one tenant. The bearer comes from the\n * `KAMINARI_AD_API_KEY` env var (parsed into `Config.stdioApiKey`).\n * All adapters are constructed once, threaded into a single\n * {@link ToolContext}, and shared across tool calls — safe because\n * there is no cross-tenant boundary in stdio mode.\n *\n * For tenant-isolation rules, see the http bootstrap and\n * CONTRIBUTING.md \"Tenant isolation\".\n */\n\nimport process from \"node:process\";\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { BearerToken } from \"../../domain/value-objects/bearer-token.js\";\nimport { newRequestId } from \"../../domain/value-objects/request-id.js\";\nimport { createHttpApiGateway } from \"../../infrastructure/api/http-api-gateway.js\";\nimport { createPinoLogger } from \"../../infrastructure/logging/pino-logger.js\";\nimport type { Config } from \"../../shared/config.js\";\nimport { SERVER_INSTRUCTIONS } from \"../../shared/server-instructions.js\";\nimport { NAME, VERSION } from \"../../shared/version.js\";\nimport { declareEmptyResourcesAndPrompts } from \"../shared/declare-empty-caps.js\";\nimport { wireToolsIntoMcpServer } from \"../shared/wire-tools.js\";\n\n/**\n * Build the stdio MCP server, connect its transport, and resolve when\n * the transport closes. Returns a process exit code.\n */\nexport async function bootstrapStdio(config: Config): Promise<number> {\n const logger = createPinoLogger(config.logLevel, config.logFormat);\n\n if (config.stdioApiKey === undefined) {\n logger.fatal({}, \"stdio.missing_api_key\");\n process.stderr.write(\n \"KAMINARI_AD_API_KEY is required in stdio mode. Generate one in Settings -> API Keys.\\n\"\n );\n return 2;\n }\n const bearer = BearerToken.fromString(config.stdioApiKey);\n if (bearer === undefined) {\n logger.fatal({}, \"stdio.invalid_api_key\");\n return 2;\n }\n\n const requestId = newRequestId();\n const scopedLogger = logger.child({ request_id: requestId, bearer_hash: bearer.hash() });\n const api = createHttpApiGateway({\n baseUrl: config.apiBaseUrl,\n bearer,\n requestId,\n logger: scopedLogger,\n });\n\n const ctx = { api, logger: scopedLogger, requestId };\n const server = new McpServer(\n { name: NAME, version: VERSION },\n { instructions: SERVER_INSTRUCTIONS }\n );\n wireToolsIntoMcpServer(server, () => ctx);\n declareEmptyResourcesAndPrompts(server);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n scopedLogger.info({}, \"stdio.ready\");\n\n await new Promise<void>((resolve) => {\n transport.onclose = (): void => {\n resolve();\n };\n });\n scopedLogger.info({}, \"stdio.shutdown\");\n return 0;\n}\n"]}
|