src/tools/operations.ts
68 lines and 7 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| define | 43 to 50 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| has | 52 to 54 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| config | 56 to 58 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| names | 60 to 62 | 1 | 33% | OkCRAP 1.3 | No data: mutation testing has not run on this repo yet |
| run | 66 to 74 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:70> | 70 to 70 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| get | 76 to 80 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by src/server.ts, src/tools/docs.ts, src/tools/surface.ts.
Source
import { z, type ZodType } from "zod";
// An input shape as the SDK takes it: field name to zod (classic) schema.
export type Shape = Record<string, ZodType>;
// The operations behind the served tools (the MCP 4 regroup).
//
// Capsid serves 14 tools and Capsid Admin 8, each one whole job with an `action`
// argument. The work each action does is an operation: a handler with its own input
// shape, defined once by the module that owns it (src/tools/docs-read.ts and the rest)
// and dispatched by the composite tool in src/tools/surface.ts. An operation is not a
// tool: nothing serves its name, scope never sees it, and an agent's tools axis names
// the composite tool and its action ("capsid_read_doc.versions"), never the operation.
//
// The dispatcher validates the arguments against the operation's own shape, which is
// what the SDK did when each operation was a tool, so a handler still receives
// arguments its schema has parsed.
export interface OperationConfig {
description: string;
inputSchema: Shape;
}
// The SDK's tool result, as the handlers build it (ok and fail in ./docs).
export interface OperationResult {
content: Array<{ type: "text"; text: string }>;
isError?: boolean;
[key: string]: unknown;
}
// Handlers keep the argument types their own shape gives them; the dispatcher has parsed
// them before the call, so the table stores each one taking unknown.
type OperationHandler = (args: unknown) => OperationResult | Promise<OperationResult>;
interface Operation {
config: OperationConfig;
handler: OperationHandler;
}
export class Operations {
private readonly table = new Map<string, Operation>();
define<S extends Shape>(
name: string,
config: { description: string; inputSchema: S },
handler: (args: z.infer<z.ZodObject<S>>) => OperationResult | Promise<OperationResult>
): void {
if (this.table.has(name)) throw new Error(`operation '${name}' is defined twice`);
this.table.set(name, { config, handler: handler as OperationHandler });
}
has(name: string): boolean {
return this.table.has(name);
}
config(name: string): OperationConfig {
return this.get(name).config;
}
names(): string[] {
return [...this.table.keys()];
}
// Parses `args` against the operation's own shape, then runs it. A parse failure is
// the refusal the SDK gave before: the argument and what was wrong with it.
async run(name: string, args: Record<string, unknown>, label: string): Promise<OperationResult> {
const op = this.get(name);
const parsed = z.object(op.config.inputSchema).safeParse(args);
if (!parsed.success) {
const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(arguments)"}: ${i.message}`).join("; ");
return { content: [{ type: "text", text: `invalid arguments for ${label}: ${issues}` }], isError: true };
}
return op.handler(parsed.data);
}
private get(name: string): Operation {
const op = this.table.get(name);
if (!op) throw new Error(`no operation '${name}'`);
return op;
}
}