/** * One `McpServer` per caller, for both transports. * * This is the whole of what the two transports share, and the reason it is * one function rather than two: every divergence this repository has shipped * between Streamable HTTP and STDIO — `permission` on one wire and not the * other, two capability sets, a cache hint on one, `ping` on one, `initialize` * negotiating on one and announcing on the other — was two dispatchers each * building a result by hand. The protocol's results are * `@modelcontextprotocol/server`'s now: it negotiates `initialize`, answers * `server/discover`, builds `tools/list`, validates arguments against the * schema, stamps `resultType` and the cache hints, and refuses a mirrored * header that lies. What is left for this repository to say is what is on the * next hundred lines, and it is said once. * * The server is built **per request** on HTTP and once per connection on * STDIO, because the catalog is per caller: the search description names the * layers this principal may read, and a server instance built for one caller * and handed to another would hand one tenant's layer names to the other. * Building one costs a single bounded page of the layer catalog, which is the * same read `tools/list` always made. * * Statelessness is kept by construction. Nothing here outlives the request * that built it; a tool that needs state between calls returns a descriptor * and takes it back as an argument. */ import { McpServer } from '@modelcontextprotocol/server'; import { type AuthContext } from '@nacre.work/api'; import { type InstructionSkill } from './instructions.js'; import { type Layer } from './tools.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; }>; } /** * The base skill an agent of this caller is given. docs/skills.md. * * Optional, so a transport built without a database — the surface suites — * serves the built-in instructions alone, which is what every deployment did * before skills existed. */ export interface SkillSource { base(auth: AuthContext): Promise; } /** * A refusal about the caller's own arguments, whose message is safe to send. * * The wrapper below hides every thrown message, because one naming a layer * says the layer exists. These name nothing the caller did not send or could * not already read: a skill the format refuses and why, a version that is not * the current one, a write this tool does not make. Answering "not found" to * any of them leaves an agent retrying a call that can never succeed. */ export declare class ToolArgumentError extends Error { readonly name = "ToolArgumentError"; } 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; } /** * 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; }; } export interface ServerBuild { /** Who is asking. Resolved by the transport before this is called, never here. */ readonly auth: AuthContext; /** * One id per request on HTTP; one per call on STDIO, where there is no * transport-level id and this is the only thing tying an audit row to one * invocation. */ readonly requestId: () => string; readonly layers: Layers; readonly tools: ToolRunner; /** The base skill for `instructions`. Absent, the built-in text alone. */ readonly skills?: SkillSource; /** What `initialize` and `server/discover` report as `serverInfo.version`. */ readonly serverVersion?: string; readonly observe?: McpMetrics; /** * Whether the client renders MCP Apps. `false` hides `upload_file`, the one * tool that does nothing without a panel; the views and the `_meta.ui` on * `search` and `list_layers` stay, because a client that does not render * them ignores them. Absent is `true` — see `Verified.ui` in server.ts. */ readonly ui?: boolean; /** * The origin the upload view sends bytes to — the API's canonical URL — * which is the one origin a view's CSP has to admit. Absent, the upload * view is not served and `upload_file` is not offered: a panel that cannot * reach the ticket endpoint is a panel that cannot do its job. */ readonly apiOrigin?: string; } /** * The four views, by the name the tool metadata and the resource list use. * * `upload` is the only one that makes a network request of its own — the * bytes to the ticket URL — so it is the only one whose CSP names an origin. * The other two reach the server through the host and nothing else, and a * CSP admitting nothing is the secure default the extension specifies. */ export declare const VIEWS: readonly ["upload", "search", "layers", "skill"]; export type View = (typeof VIEWS)[number]; export declare const viewUri: (view: View) => string; /** * A view's HTML, read from `apps/build/` — one directory above this module * whether it runs from `src/` under the test runner or from `dist/` in the * package, which is why the path is relative to the module and not to the * working directory. Built by `scripts/build-apps.mjs`; a missing file is a * build that did not run, and says so. */ /** * The administrative surface's views: the change panel every write opens, and * the four a read opens. Served on `/mcp/admin` only; the ordinary surface * lists none of them. */ export declare const ADMIN_VIEWS: readonly ["change", "audit", "connections", "access", "layer", "grants"]; export type AdminView = (typeof ADMIN_VIEWS)[number]; export declare function viewHtml(view: View | AdminView): Promise; /** * The catalog for this caller, and a server that serves it. * * A bounded page: the search description names a handful of layers and says * there are more, rather than interpolating a catalog that is a million * entries on the installations layers are sold for. */ export declare function buildServer(build: ServerBuild): Promise; //# sourceMappingURL=factory.d.ts.map