import { type Server } from 'node:http'; import { type ProtectedResourceMetadata } from '@nacre.work/core'; import { type AuthContext, type LimitPolicy, type RateLimiter, type Resource, type VerifyOptions } from '@nacre.work/api'; import { type Layer } from './tools.js'; export { DISCOVER_TTL_MS, LEGACY_PROTOCOL_VERSIONS, PROTOCOL_VERSION, PROTOCOL_VERSIONS, TOOLS_TTL_MS, } from './results.js'; export interface Layers { /** * One page of the layers this caller may read, ordered by id. * * A page and never the whole catalog, and the bound is **required** rather * than defaulted: this used to return every layer the plan reaches, which on * an installation at the scale layers are sold for — one per patient, one * per matter — is a million-row answer built per call. `afterId` is the seek; * `nextCursor` is the last id when another page exists. Drives `list_layers` * and the search description, each at its own bound. */ forCaller(auth: AuthContext, page: { readonly limit: number; readonly afterId?: string; }): Promise<{ readonly layers: readonly Layer[]; readonly nextCursor: string | null; }>; } export interface ToolRunner { /** * `requestId` is threaded through so audit rows can be joined to a request. * * Every MCP audit row carried the literal string `mcp` — this transport * generates a real id per request and never passed it down, so * `docs/config.md`'s claim that "an auditor's question and a latency * investigation resolve against the same identifier" was true of REST and * false here. */ call(name: string, args: Record, auth: AuthContext, requestId: string): Promise; } export interface McpOptions { /** * `serviceKeys` is required here, unlike on the REST surface. * * This transport exists for agents, and an agent authenticates with a service * account key. Leaving the resolver out is not a smaller deployment — it is a * server that 401s every `nacre_sk_` token while the same key works over REST * and STDIO, which reads as a revoked credential rather than a missing wire. * Requiring it makes that a compile error instead of a support ticket. */ readonly verify: VerifyOptions & { readonly serviceKeys: NonNullable; }; readonly layers: Layers; readonly tools: ToolRunner; /** Where a 401 points the client for discovery, per RFC 9728. */ readonly resourceMetadataUrl: string; /** The document that URL resolves to. Built once, in main, and shared with the API. */ readonly resourceMetadata: ProtectedResourceMetadata; /** * What `initialize` reports as `serverInfo.version`. * * Informational, and passed in rather than read from a manifest here: this * module is imported by tests and by the STDIO entry point as well as by the * server, and a file read at import time is the shape that threw ENOENT from * the built package once already. */ readonly serverVersion?: string; /** * Build the discovery document from the origin the client actually reached, * rather than from one baked in at startup. * * Set only when the deployment has **not** pinned `NACRE_MCP_CANONICAL_URL`. * RFC 9728 has the client compare the `resource` identifier against the URL * it used, so a document naming anything else is refused before a token is * ever sent — and the Compose default named `http://localhost:8081`, which is * wrong for every client that is not on the server's own machine. A default * that quietly points at localhost is the failure `loadConfig` refuses for * every other URL in this product, and it had been introduced here. * * Deriving from `Host` is not a trust decision: the identifier is not an * authorization input. A token is still checked against `NACRE_JWT_AUDIENCE` * and `NACRE_JWT_ISSUER`, neither of which comes from the request, so the * worst a forged `Host` achieves is a document naming a resource whose tokens * this server will not accept. */ readonly resourceFromRequest?: (origin: string) => ProtectedResourceMetadata; /** * Browser origins this transport answers, from `NACRE_MCP_ALLOWED_ORIGINS`. * * Validating `Origin` is a MUST in the specification and the attack it names * is DNS rebinding: a page in somebody's browser reaching an MCP server on * their network. Absent means no browser origin is allowed, which is the * right default for a transport built for agents — an agent sends no * `Origin` and is unaffected. */ readonly allowedOrigins?: readonly string[]; /** * The same limiter and the same policies the REST surface uses. * * Absent means unlimited, which is what this transport was: `NACRE_RATE_*` * applied to REST only, so a client that had run out of search budget could * point at port 8081 and carry on. Two doors into one authorization service, * one of them with a lock on it. * * Counted per organization on the same keys, deliberately — a shared bucket * rather than one bucket per surface. Splitting them would give a caller * twice the documented allowance for holding two clients, which is the same * hole one level up. */ readonly limits?: RateLimiter; readonly limitPolicies?: Readonly>; /** Rendered at `/metrics`. Absent means the endpoint answers 404. */ readonly metrics?: { render(): Promise; }; /** A bearer token required on `/metrics`. Absent leaves it open. */ readonly metricsToken?: string; /** Where the tool path writes what it measured. */ readonly observe?: McpMetrics; } /** * What this transport records. * * It recorded nothing. The MCP server built no registry and served no * `/metrics`, so every claim in `docs/config.md` about search latency and * denials was true of REST and silent here — and this is the transport the * product is *for*. An agent's search was invisible: not slow, not failing, * absent. */ export interface McpMetrics { toolDuration: { observe(seconds: number, labels?: Record): void; }; toolCalls: { inc(labels?: Record, by?: number): void; }; aclDenials: { inc(labels?: Record, by?: number): void; }; authFailures: { inc(labels?: Record, by?: number): void; }; } /** * Streamable HTTP, one endpoint, no session. * * There is no `initialize`, no `Mcp-Session-Id`, and nothing kept between * requests — which is what lets any replica behind a round-robin balancer serve * any request. A tool that needs state between calls returns an explicit * descriptor and takes it back as an argument; hidden state in the transport * would quietly reintroduce the affinity the deployment model rules out. */ export declare function createMcpServer(options: McpOptions): Server; //# sourceMappingURL=server.d.ts.map