@kaminari-ad/mcp 0.7.2 → 0.9.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.
@@ -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.7.2";
6
+ var VERSION = "0.9.0";
7
7
 
8
8
  export { NAME, VERSION };
9
- //# sourceMappingURL=chunk-VTKVXW4G.js.map
10
- //# sourceMappingURL=chunk-VTKVXW4G.js.map
9
+ //# sourceMappingURL=chunk-SWFPWJ74.js.map
10
+ //# sourceMappingURL=chunk-SWFPWJ74.js.map
@@ -1 +1 @@
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
+ {"version":3,"sources":["../src/shared/version.ts"],"names":[],"mappings":";;;;AASO,IAAM,IAAA,GAAO;AACb,IAAM,OAAA,GAAU","file":"chunk-SWFPWJ74.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.9.0\";\n"]}
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-NRDA7DQQ.js';
3
- import { VERSION, NAME } from './chunk-VTKVXW4G.js';
2
+ import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-E66U4JXR.js';
3
+ import { VERSION, NAME } from './chunk-SWFPWJ74.js';
4
4
  import { createServer } from 'http';
5
5
  import process from 'process';
6
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -234,5 +234,5 @@ async function bootstrapHttp(config) {
234
234
  }
235
235
 
236
236
  export { bootstrapHttp };
237
- //# sourceMappingURL=http-bootstrap-7VZLKAM4.js.map
238
- //# sourceMappingURL=http-bootstrap-7VZLKAM4.js.map
237
+ //# sourceMappingURL=http-bootstrap-ZXECSCYN.js.map
238
+ //# sourceMappingURL=http-bootstrap-ZXECSCYN.js.map
@@ -1 +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
+ {"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-ZXECSCYN.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-NRDA7DQQ.js';
3
- import { VERSION, NAME } from './chunk-VTKVXW4G.js';
2
+ import { createPinoLogger, BearerToken, newRequestId, createHttpApiGateway, SERVER_INSTRUCTIONS, wireToolsIntoMcpServer, declareEmptyResourcesAndPrompts } from './chunk-E66U4JXR.js';
3
+ import { VERSION, NAME } from './chunk-SWFPWJ74.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-CE2Y76DX.js.map
51
- //# sourceMappingURL=stdio-bootstrap-CE2Y76DX.js.map
50
+ //# sourceMappingURL=stdio-bootstrap-6DXIUJTV.js.map
51
+ //# sourceMappingURL=stdio-bootstrap-6DXIUJTV.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-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"]}
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-6DXIUJTV.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.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Official Model Context Protocol (MCP) server for the Kaminari Ad ad-verification platform.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -124,6 +124,6 @@
124
124
  },
125
125
  "overrides": {
126
126
  "esbuild": "0.28.1",
127
- "js-yaml": "4.2.0"
127
+ "js-yaml": "4.3.0"
128
128
  }
129
129
  }