Skip to the content

Capsid menu

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

The functions of server.ts
FunctionLinesComplexityCoveredCRAPMutation
concurrentEditWarning32 to 465100%OkCRAP 5No data: mutation testing has not run on this repo yet
buildServer53 to 551100%OkCRAP 1No data: mutation testing has not run on this repo yet
buildAdminServer61 to 631100%OkCRAP 1No data: mutation testing has not run on this repo yet
build65 to 1274100%OkCRAP 4No data: mutation testing has not run on this repo yet
lastActor78 to 842100%OkCRAP 2No data: mutation testing has not run on this repo yet
scope93 to 931100%OkCRAP 1No 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;
}