src/server.ts
112 lines and 6 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. It is in an import cycle. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| concurrentEditWarning | 32 to 46 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| buildServer | 53 to 55 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| buildAdminServer | 61 to 63 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| build | 65 to 127 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| lastActor | 78 to 84 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| scope | 93 to 93 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/admin-client-audit.ts, src/agents.ts, src/cache-hints.ts, src/env.ts, src/prompts.ts, src/resources.ts, src/scope.ts, src/time.ts, src/tools/admin.ts, src/tools/agents.ts, src/tools/claims.ts, src/tools/cloudflare.ts, src/tools/controls.ts, src/tools/docs.ts, src/tools/jobs.ts, src/tools/lint.ts, src/tools/operations.ts, src/tools/ops.ts, src/tools/repo.ts, src/tools/status.ts, src/tools/surface.ts. Imported by src/index.ts, src/routes.ts, src/tools/docs-write.ts.
Source
import { McpServer } from "@modelcontextprotocol/server";
import type { Env } from "./env";
import { legacyAgent, type Agent } from "./agents";
import { checkScope, guardRegistrations } from "./scope";
import type { AdminWriteObserver } from "./admin-client-audit";
import { registerDocumentResources } from "./resources";
import { registerPrompts } from "./prompts";
import { CACHE_HINTS } from "./cache-hints";
import { defineDocOperations, type ToolCtx, type ToolGrant } from "./tools/docs";
import { defineLintOperations } from "./tools/lint";
import { defineRepoOperations } from "./tools/repo";
import { defineStatusOperations } from "./tools/status";
import { defineJobOperations } from "./tools/jobs";
import { defineAgentOperations } from "./tools/agents";
import { defineClaimsOperations } from "./tools/claims";
import { defineOpsSnapshotOperations } from "./tools/ops";
import { defineCloudflareOperations } from "./tools/cloudflare";
import { defineControlOperations } from "./tools/controls";
import { defineAdminOperations } from "./tools/admin";
import { Operations } from "./tools/operations";
import { registerCapsidAdminTools, registerCapsidTools } from "./tools/surface";
import { HOUR_MS } from "./time";
export type { ToolGrant };
const SERVER_INFO = { name: "capsid", version: "1.0.0" };
const ADMIN_SERVER_INFO = { name: "capsid-admin", version: "1.0.0" };
// Warn, never refuse, when overwriting a document touched in the last hour without if_match.
const CONCURRENT_EDIT_WINDOW_MS = HOUR_MS;
export function concurrentEditWarning(updatedAt: string | null | undefined, now: number): string | null {
if (!updatedAt) return null;
// D1 stores datetime('now') in UTC with no zone, which Date.parse reads as local.
// That would silence the warning on a machine behind UTC and fire it constantly on
// one ahead.
const parsed = Date.parse(`${updatedAt.replace(" ", "T")}Z`);
if (Number.isNaN(parsed)) return null;
const age = now - parsed;
if (age < 0 || age > CONCURRENT_EDIT_WINDOW_MS) return null;
return (
`possible concurrent edit: this document was last written at ${updatedAt} UTC, within the last hour, and no if_match was passed. ` +
`The write went through and the prior body was snapshotted to document_versions, so nothing is lost, but if another session is working on this document your write may have just replaced its changes. ` +
`Pass if_match (the sha256 this response returns) on the next write to make that a refusal instead of a warning.`
);
}
// The caller is an agent (src/agents.ts). A bare grant plus an actor string means an
// unrestricted caller at that grant: the OPERATOR_KEY_HASH fallback, expressed once so
// there is no second code path where scopes do not apply.
const MAX_TOOL_INPUT_ELEMENTS = 10_000;
export function buildServer(env: Env, caller: Agent | ToolGrant, actor = "", observeAdminWrite?: AdminWriteObserver): McpServer {
return build(env, caller, actor, observeAdminWrite, "capsid");
}
// Capsid Admin: the eight
// administrator tools, on their own route for the administrator alone
// (src/mcp-handler.ts). Every tool on it is admin only in TOOL_GRANTS too, so the route
// and the registrar refuse a non-admin twice.
export function buildAdminServer(env: Env, caller: Agent | ToolGrant, actor = "", observeAdminWrite?: AdminWriteObserver): McpServer {
return build(env, caller, actor, observeAdminWrite, "admin");
}
function build(env: Env, caller: Agent | ToolGrant, actor: string, observeAdminWrite: AdminWriteObserver | undefined, surface: "capsid" | "admin"): McpServer {
const agent = typeof caller === "string" ? legacyAgent(caller, actor) : caller;
// maxToolInputElements caps how many array elements and object members one tools/call may
// carry, which the SDK leaves unlimited (GHSA-46jr-4wx8-v453; Grok audit 2026-10-21, CA-2).
// A document body is one string, so one element; 10,000 leaves room for any list a tool takes.
const server = new McpServer(surface === "admin" ? ADMIN_SERVER_INFO : SERVER_INFO, { cacheHints: CACHE_HINTS, maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS });
const db = env.DB;
// Provenance: the actor from a document's latest audit_log entry, surfaced by read
// and brief so a session can tell who wrote what it treats as context: a document
// another client wrote is untrusted input. A separate read rather than a joined
// subquery, so the document read stays a named-column projection. Null when the
// document has no audit history.
const lastActor = async (ns: string, path: string): Promise<string | null> => {
const row = await db
.prepare("SELECT actor FROM audit_log WHERE namespace = ?1 AND path = ?2 ORDER BY id DESC LIMIT 1")
.bind(ns, path)
.first<{ actor: string | null }>();
return row?.actor ?? null;
};
const ctx: ToolCtx = {
env,
db,
actor: agent.actor,
agent,
// The handler half of the one enforcement point, for checks that need the action,
// mode or path.
scope: (need) => checkScope(agent, need),
lastActor,
ops: new Operations(),
};
// Every operation, on both servers: an operation is not served until a tool on this
// server dispatches to it (src/tools/surface.ts).
defineDocOperations(server, ctx);
defineLintOperations(server, ctx);
defineRepoOperations(server, ctx);
defineStatusOperations(server, ctx);
defineJobOperations(server, ctx);
defineAgentOperations(server, ctx);
defineClaimsOperations(server, ctx);
defineOpsSnapshotOperations(server, ctx);
defineCloudflareOperations(server, ctx);
defineControlOperations(server, ctx);
defineAdminOperations(server, ctx);
// Before any registration, so every tool registered below is wrapped.
guardRegistrations(server, agent, observeAdminWrite, env.APP_KV);
if (surface === "admin") {
registerCapsidAdminTools(server, ctx.ops);
return server;
}
registerCapsidTools(server, ctx.ops);
// Resources and prompts are raw request handlers, not tool registrations, so they
// check scope themselves (src/resources.ts).
registerDocumentResources(server, agent, db);
registerPrompts(server, agent, db);
return server;
}