@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.
@@ -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.5.2";
6
+ var VERSION = "0.7.2";
7
7
 
8
8
  export { NAME, VERSION };
9
- //# sourceMappingURL=chunk-SQVFFWOB.js.map
10
- //# sourceMappingURL=chunk-SQVFFWOB.js.map
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-SQVFFWOB.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.5.2\";\n"]}
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-3CY2WEZF.js';
3
- import { VERSION, NAME } from './chunk-SQVFFWOB.js';
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
- const seedTarget = {};
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
- sessionIdGenerator: () => randomUUID(),
137
- enableJsonResponse: true,
138
- onsessioninitialized: (issued) => {
139
- const sid = parseSessionId(issued);
140
- if (sid === void 0) return;
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
- transport.onclose = () => {
152
- for (const [sid, entry] of liveSessions) {
153
- if (entry.transport === transport) liveSessions.delete(sid);
154
- }
155
- };
156
- await server.connect(transport);
157
- return { server, transport, ctxRef };
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, sessions, rateLimiter } = deps;
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
- writeJson2(res, 200, { status: "ok" });
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
- writeJson2(res, 404, { error: "Not found" });
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
- writeJson2(
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
- writeJson2(res, 429, { error: "Rate limited" }, headers);
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 entry = existingEntry ?? await initNewSession({ requestId, reqLogger, liveSessions, sessions, bearer });
289
- entry.ctxRef.current = { api, logger: reqLogger, requestId };
160
+ const { server, transport } = await createStatelessMcp({ api, logger: reqLogger, requestId });
290
161
  try {
291
- await entry.transport.handleRequest(req, res);
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
- writeJson2(res, 500, { error: "Internal server error" });
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 writeJson2(res, status, body, extraHeaders = {}) {
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, sessions, rateLimiter });
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-X2Y2VGBO.js.map
365
- //# sourceMappingURL=http-bootstrap-X2Y2VGBO.js.map
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-3CY2WEZF.js';
3
- import { VERSION, NAME } from './chunk-SQVFFWOB.js';
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-JBLNH46R.js.map
51
- //# sourceMappingURL=stdio-bootstrap-JBLNH46R.js.map
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-JBLNH46R.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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaminari-ad/mcp",
3
- "version": "0.5.2",
3
+ "version": "0.7.2",
4
4
  "description": "Official Model Context Protocol (MCP) server for the Kaminari Ad ad-verification platform.",
5
5
  "keywords": [
6
6
  "mcp",