synartesis 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/dist/chunk-K3QIPVBY.js +85 -0
- package/dist/chunk-K3QIPVBY.js.map +1 -0
- package/dist/chunk-X4VQNEP5.js +1383 -0
- package/dist/chunk-X4VQNEP5.js.map +1 -0
- package/dist/cli.js +1708 -0
- package/dist/cli.js.map +1 -0
- package/dist/demo-agent.js +67 -0
- package/dist/demo-agent.js.map +1 -0
- package/dist/proxy.js +913 -0
- package/dist/proxy.js.map +1 -0
- package/dist/toy-crm.js +293 -0
- package/dist/toy-crm.js.map +1 -0
- package/manifests/filesystem.yaml +97 -0
- package/manifests/git.yaml +77 -0
- package/manifests/github.yaml +175 -0
- package/manifests/memory.yaml +96 -0
- package/manifests/toy-crm.yaml +66 -0
- package/package.json +67 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proxy/stdio.ts","../src/gate/gate.ts","../src/logging.ts","../src/proxy/proxy.ts","../src/gate/heuristic.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * better-sqlite3 requires Node 22, and on Node 20 it does not fail politely:\n * it segfaults the moment a database is opened. Saying so is better than\n * letting somebody meet exit code 139.\n */\nconst NODE_MAJOR = Number(process.versions.node.split(\".\")[0]);\nif (NODE_MAJOR < 22) {\n process.stderr.write(\n `synartesis: needs Node 22 or newer, and this is ${process.version}.\\n`,\n );\n process.exit(2);\n}\n\nimport { resolve } from \"node:path\";\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { describe } from \"../errors.js\";\nimport { DEFAULT_GATE_TIMEOUT_MS } from \"../gate/gate.js\";\nimport { cliCommandFrom } from \"../invocation.js\";\nimport { findJournal, findManifest } from \"../locate.js\";\nimport { createLogger, isLogLevel, LOG_LEVELS, type LogLevel } from \"../logging.js\";\nimport { mark } from \"../style.js\";\nimport { openJournal } from \"../journal/journal.js\";\nimport { loadManifest } from \"../manifest/load.js\";\nimport { verifyAgainstServers } from \"../manifest/verify.js\";\nimport { createProxyServer } from \"./proxy.js\";\nimport { connectStdioUpstream, type Upstream } from \"./upstream.js\";\n\n/**\n * The manifest is the configuration (D3): it already declares every server and\n * how to start it, so there is nothing left for flags to say.\n *\n * synartesis-proxy [--manifest synartesis.yaml] [--journal .synartesis/journal.db]\n * [--gate-timeout <seconds>] [--log-level <level>]\n */\ninterface Argv {\n readonly manifest: string;\n readonly journal: string;\n readonly gateTimeoutMs: number;\n readonly logLevel: LogLevel;\n}\n\nfunction parseArgv(argv: readonly string[]): Argv {\n const read = (flag: string): string | undefined => {\n const at = argv.indexOf(flag);\n return at === -1 ? undefined : argv[at + 1];\n };\n const known = [\"--manifest\", \"--journal\", \"--gate-timeout\", \"--log-level\"];\n const unknown = argv.find((token) => token.startsWith(\"--\") && !known.includes(token));\n if (unknown !== undefined) {\n throw new Error(`unknown flag ${unknown}; expected one of ${known.join(\", \")}`);\n }\n\n const rawTimeout = read(\"--gate-timeout\");\n const seconds = rawTimeout === undefined ? undefined : Number(rawTimeout);\n if (seconds !== undefined && (!Number.isFinite(seconds) || seconds <= 0)) {\n throw new Error(\"--gate-timeout needs a positive number of seconds\");\n }\n\n const level = read(\"--log-level\") ?? \"info\";\n if (!isLogLevel(level)) {\n throw new Error(`--log-level must be one of ${LOG_LEVELS.join(\", \")}`);\n }\n\n const manifest = findManifest(read(\"--manifest\"));\n return {\n manifest,\n journal: findJournal(read(\"--journal\"), manifest),\n gateTimeoutMs: seconds === undefined ? DEFAULT_GATE_TIMEOUT_MS : seconds * 1000,\n logLevel: level,\n };\n}\n\nasync function main(): Promise<void> {\n const argv = parseArgv(process.argv.slice(2));\n const log = createLogger(argv.logLevel);\n // Only on a real terminal. A client collecting our stderr into a log file\n // wants the structured records and nothing else.\n if (process.stderr.isTTY) {\n process.stderr.write(mark());\n }\n // Loaded before anything is spawned: never start with a broken policy.\n const manifest = loadManifest(argv.manifest);\n const journal = openJournal(argv.journal);\n\n const upstreams: Upstream[] = [];\n for (const [name, spec] of Object.entries(manifest.servers)) {\n upstreams.push(\n await connectStdioUpstream({\n name,\n command: spec.command,\n args: spec.args,\n ...(spec.env === undefined ? {} : { env: spec.env }),\n }),\n );\n }\n\n // Never serve a request under a policy that calls tools the servers do not\n // have: at run time that is indistinguishable from a missing resource.\n await verifyAgainstServers(upstreams, manifest);\n\n log.info(\n {\n manifest: argv.manifest,\n journal: argv.journal,\n servers: upstreams.map((upstream) => upstream.name),\n policies: manifest.tools.length,\n },\n \"proxy ready\",\n );\n\n const proxy = createProxyServer({\n upstreams,\n manifest,\n journal,\n gateTimeoutMs: argv.gateTimeoutMs,\n logger: log,\n // Absolute, because whoever approves may be in any directory at all.\n approveHint: (actionId: string): string =>\n `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,\n });\n\n let shuttingDown = false;\n const shutdown = (code: number): void => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n void (async (): Promise<void> => {\n // Let in-flight calls settle before tearing the connection down. An\n // aborted write leaves the journal unable to say whether it applied.\n await Promise.race([\n proxy.whenIdle(),\n new Promise<void>((resolve) => setTimeout(resolve, 5000).unref()),\n ]);\n await proxy.server.close();\n for (const upstream of upstreams) {\n await upstream.close();\n }\n journal.close();\n process.exit(code);\n })();\n };\n\n process.on(\"SIGINT\", () => {\n shutdown(0);\n });\n process.on(\"SIGTERM\", () => {\n shutdown(0);\n });\n\n // StdioServerTransport only reports a close that we initiate; it never\n // reacts to the parent closing the pipe. Without these listeners the proxy\n // survives its own client, holding every upstream child open until whoever\n // spawned us escalates to a signal.\n // The pipe closing means no more requests are coming, not that the ones\n // already delivered can be dropped. The transport hands only a few buffered\n // frames to handlers per turn of the event loop, so wait until the proxy has\n // been quiet for several consecutive turns rather than yielding a fixed\n // number of times, which is guesswork. The cap stops a wedged upstream from\n // holding the process open.\n const pipeClosed = (): void => {\n const giveUpAt = Date.now() + 5000;\n let quiet = 0;\n const settle = (): void => {\n quiet = proxy.busy() ? 0 : quiet + 1;\n if (quiet >= 10 || Date.now() > giveUpAt) {\n shutdown(0);\n return;\n }\n setImmediate(settle);\n };\n setImmediate(settle);\n };\n process.stdin.on(\"end\", pipeClosed);\n process.stdin.on(\"close\", pipeClosed);\n\n const inner = proxy.server.server;\n const onclose = inner.onclose;\n inner.onclose = (): void => {\n onclose?.();\n shutdown(0);\n };\n\n await proxy.server.connect(new StdioServerTransport());\n}\n\ntry {\n await main();\n} catch (error: unknown) {\n // stdout carries protocol frames only; diagnostics must not corrupt it.\n process.stderr.write(`synartesis: ${describe(error)}\\n`);\n process.exit(1);\n}\n","import type { Journal } from \"../journal/journal.js\";\n\nexport interface GateRequest {\n readonly actionId: string;\n readonly runId: string;\n readonly seq: number;\n readonly server: string;\n readonly tool: string;\n readonly args: unknown;\n /** Why this is being asked about, in the words the agent is given. */\n readonly why: string;\n readonly signal: AbortSignal;\n}\n\nexport type GateDecision =\n | { readonly approved: true; readonly by: string }\n | {\n readonly approved: false;\n readonly by?: string;\n readonly reason: string;\n /**\n * Nobody has refused; the request is simply waiting for a person. The\n * agent should tell its user how to approve and then try again.\n */\n readonly awaiting?: boolean;\n };\n\nexport interface Gate {\n decide(request: GateRequest): Promise<GateDecision>;\n}\n\nexport const DEFAULT_GATE_TIMEOUT_MS = 300_000;\n\n/**\n * Records the request and refuses immediately, rather than holding the call\n * open until someone answers.\n *\n * Holding it open cannot work against a real client. Measured against Claude\n * Code: a suspended call sat for the full five minutes while the client had\n * long since reported it as failed, and any approval in that gap would have\n * sent something the agent had already said it had not sent. Every useful\n * window for a person to notice, open a terminal and decide is longer than a\n * client will wait, so the two cannot be reconciled by choosing a better\n * timeout. Refusing at once and letting the agent retry removes the conflict\n * instead of tuning it.\n */\n/**\n * `approveHint` builds the command a person on this machine would actually\n * run, journal path and all. A hint that omits an argument the caller needs is\n * an instruction that fails the moment somebody follows it.\n */\nexport type ApproveHint = (actionId: string) => string;\n\nconst DEFAULT_HINT: ApproveHint = (actionId) => `synartesis approve ${actionId.slice(0, 8)}`;\n\nexport function createRetryGate(journal: Journal, approveHint: ApproveHint = DEFAULT_HINT): Gate {\n return {\n decide(request: GateRequest): Promise<GateDecision> {\n journal.markGated(request.actionId, request.why);\n return Promise.resolve({\n approved: false,\n awaiting: true,\n reason:\n \"it is waiting for a person to approve it. Ask them to run: \" +\n approveHint(request.actionId) +\n \" --- then make this exact call again.\",\n });\n },\n };\n}\n\nexport interface JournalGateOptions {\n readonly timeoutMs?: number;\n readonly pollMs?: number;\n /** Where the operator is told that something is waiting. */\n readonly notify?: (request: GateRequest) => void;\n}\n\n/**\n * Approval arrives out of band, through the journal, rather than from a prompt\n * on stdin.\n *\n * The proxy speaks MCP over stdin and stdout: that pipe carries protocol\n * frames, so there is nothing to prompt on. A prompt written to the\n * controlling terminal would work only when one exists, which rules out every\n * desktop client. The journal is already a transactional, WAL-mode, multi\n * process store, so `synartesis approve` in any other terminal is the natural\n * channel, and it behaves identically wherever the proxy was launched from.\n */\nexport function createJournalGate(journal: Journal, options: JournalGateOptions = {}): Gate {\n const timeoutMs = options.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;\n const pollMs = options.pollMs ?? 100;\n const notify = options.notify ?? ((): void => undefined);\n\n return {\n async decide(request: GateRequest): Promise<GateDecision> {\n journal.markGated(request.actionId, request.why);\n notify(request);\n\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const action = journal.getAction(request.actionId);\n if (action === undefined) {\n return { approved: false, reason: \"the journal entry disappeared while awaiting approval\" };\n }\n if (action.status !== \"gated\") {\n return action.status === \"denied\"\n ? {\n approved: false,\n ...(action.approvedBy === undefined ? {} : { by: action.approvedBy }),\n reason: action.error ?? \"denied\",\n }\n : { approved: true, by: action.approvedBy ?? \"unknown\" };\n }\n\n if (request.signal.aborted) {\n journal.deny(request.actionId, undefined, \"the client disconnected before a decision\");\n return { approved: false, reason: \"the client disconnected before a decision\" };\n }\n if (Date.now() >= deadline) {\n // Deny by default (3.4): silence is not consent.\n const reason = `no answer within ${String(Math.round(timeoutMs / 1000))}s, so it was denied`;\n journal.deny(request.actionId, undefined, reason);\n return { approved: false, reason };\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, pollMs).unref());\n }\n },\n };\n}\n","import pino, { type Logger } from \"pino\";\n\nexport type { Logger };\n\nexport const LOG_LEVELS = [\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"silent\"] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nexport function isLogLevel(value: string): value is LogLevel {\n return LOG_LEVELS.some((level) => level === value);\n}\n\n/**\n * Always fd 2. stdout carries MCP protocol frames, and a single stray log line\n * on it corrupts the session for every client. Synchronous so that the last\n * lines before an exit are not lost, which is exactly when they matter.\n */\nexport function createLogger(level: LogLevel): Logger {\n return pino(\n { level, base: { name: \"synartesis\" } },\n pino.destination({ dest: 2, sync: true }),\n );\n}\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport {\n CallToolRequestSchema,\n CompleteRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n ListPromptsRequestSchema,\n ListResourceTemplatesRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n SetLevelRequestSchema,\n SubscribeRequestSchema,\n UnsubscribeRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type {\n Implementation,\n Request,\n ServerCapabilities,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { SnapshotError, UpstreamError, describe } from \"../errors.js\";\nimport { createRetryGate, type ApproveHint, type Gate } from \"../gate/gate.js\";\nimport { shouldGateOnWrite } from \"../gate/heuristic.js\";\nimport type { Journal } from \"../journal/journal.js\";\nimport type { Logger } from \"../logging.js\";\nimport {\n createPolicyResolver,\n type PolicyResolver,\n} from \"../manifest/match.js\";\nimport { qualify, type Manifest } from \"../manifest/types.js\";\nimport { createRouter, type Router } from \"./routing.js\";\nimport {\n observeState,\n planInverse,\n isDisconnected,\n mayHaveArrived,\n planRead,\n refusal,\n runRead,\n toPayload,\n type ResolvedRead,\n} from \"./snapshot.js\";\nimport type { Upstream } from \"./upstream.js\";\n\nexport interface ProxyOptions {\n readonly upstreams: readonly Upstream[];\n readonly manifest: Manifest;\n readonly journal: Journal;\n /** Defaults to out-of-band approval through the journal. */\n readonly gate?: Gate;\n readonly gateTimeoutMs?: number;\n readonly logger?: Logger;\n /** Builds the exact command a person here would run to approve an action. */\n readonly approveHint?: ApproveHint;\n}\n\nexport interface ProxyServer {\n readonly server: McpServer;\n /** Resolves with the run id once the client session is initialized. */\n readonly ready: Promise<string>;\n /** Resolves when no tool call is in flight, so shutdown can drain first. */\n whenIdle(): Promise<void>;\n /** The open run, once the session has initialized. */\n readonly runId: string | undefined;\n /** Whether any forwarded request is currently in flight. */\n busy(): boolean;\n}\n\ntype Passthrough = { [key: string]: unknown };\n\n/**\n * How long an approval stays usable. Long enough to survive a client restart\n * and a person walking away from their desk, short enough that a decision made\n * this morning cannot quietly authorise the same call tomorrow.\n */\nconst APPROVAL_WINDOW_MS = 60 * 60 * 1000;\n\n/**\n * Results are read through loose schemas. The SDK's typed schemas strip fields\n * they do not know about, which would quietly erase any metadata an upstream\n * added; only the names this proxy has to rewrite are described here.\n */\nconst PassthroughResult = z.looseObject({});\nconst ToolList = z.looseObject({\n tools: z.array(z.looseObject({ name: z.string() })),\n nextCursor: z.string().optional(),\n});\nconst PromptList = z.looseObject({\n prompts: z.array(z.looseObject({ name: z.string() })),\n nextCursor: z.string().optional(),\n});\nconst ResourceList = z.looseObject({\n resources: z.array(z.looseObject({ uri: z.string() })),\n nextCursor: z.string().optional(),\n});\nconst TemplateList = z.looseObject({\n resourceTemplates: z.array(z.looseObject({ uriTemplate: z.string() })),\n nextCursor: z.string().optional(),\n});\n\nfunction unwrap(error: McpError): string {\n const prefix = `MCP error ${String(error.code)}: `;\n return error.message.startsWith(prefix)\n ? error.message.slice(prefix.length)\n : error.message;\n}\n\nfunction rethrow(server: string, operation: string, error: unknown): never {\n if (error instanceof McpError) {\n throw new McpError(error.code, unwrap(error), error.data);\n }\n throw new UpstreamError(server, operation, error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The client sees one logical server, so it must be told about anything any\n * upstream can do. Sub-objects are merged rather than replaced so that, for\n * example, one server's resources.subscribe survives another's resources {}.\n */\nfunction mergeCapabilities(\n all: readonly ServerCapabilities[],\n): ServerCapabilities {\n const merged: Record<string, unknown> = {};\n for (const capabilities of all) {\n for (const [key, value] of Object.entries(capabilities)) {\n const existing = merged[key];\n merged[key] =\n isRecord(existing) && isRecord(value)\n ? { ...existing, ...value }\n : value;\n }\n }\n return merged;\n}\n\nfunction identityFor(router: Router): Implementation {\n const only = router.upstreams[0];\n if (!router.prefixed && only !== undefined) {\n const upstream = only.client.getServerVersion();\n if (upstream !== undefined) {\n return upstream;\n }\n }\n // With several servers behind it there is no single identity to mirror.\n return { name: \"synartesis\", version: \"0.0.0\" };\n}\n\n/**\n * Told to the agent at connect time. Without it a gated call is just an opaque\n * failure, and the person watching has no idea why their agent stopped or what\n * they are supposed to do about it. With it, the agent explains itself.\n */\nconst SYNARTESIS_INSTRUCTIONS = [\n \"These tools are guarded by Synartesis, which records every change so it can be undone later.\",\n \"\",\n \"Some actions cannot be undone. Those are held until a person approves them, and the call\",\n \"will fail with a message beginning \\\"Synartesis is holding this call for approval\\\".\",\n \"When that happens:\",\n \" 1. Tell the user plainly that you are asking Synartesis for approval, and what for.\",\n \" 2. Give them the exact `synartesis approve ...` command from the error.\",\n \" 3. Once they say they have approved it, make the same call again. It will go through.\",\n \"Do not try to work around a held call by using a different tool to achieve the same thing.\",\n].join(\"\\n\");\n\nfunction instructionsFor(router: Router): string {\n const sections = router.upstreams\n .map((upstream) => ({\n name: upstream.name,\n text: upstream.client.getInstructions(),\n }))\n .filter(\n (section): section is { name: string; text: string } => section.text !== undefined,\n );\n\n const upstream = router.prefixed\n ? sections.map((section) => `Tools prefixed ${section.name}__:\\n${section.text}`).join(\"\\n\\n\")\n : (sections[0]?.text ?? \"\");\n\n return upstream === \"\" ? SYNARTESIS_INSTRUCTIONS : `${SYNARTESIS_INSTRUCTIONS}\\n\\n${upstream}`;\n}\n\n/** Walks every page so that aggregation across servers is never partial. */\nasync function drain<T>(\n fetch: (\n cursor: string | undefined,\n ) => Promise<{ items: T[]; nextCursor: string | undefined }>,\n): Promise<T[]> {\n const collected: T[] = [];\n let cursor: string | undefined;\n do {\n const page = await fetch(cursor);\n collected.push(...page.items);\n cursor = page.nextCursor;\n } while (cursor !== undefined);\n return collected;\n}\n\nexport function createProxyServer(options: ProxyOptions): ProxyServer {\n const { upstreams, manifest, journal } = options;\n const router = createRouter(upstreams, manifest);\n const policies: PolicyResolver = createPolicyResolver(manifest);\n\n const log = options.logger;\n\n const gate = options.gate ?? createRetryGate(journal, options.approveHint);\n\n const capabilities = mergeCapabilities(\n upstreams.map((upstream) => upstream.client.getServerCapabilities() ?? {}),\n );\n const instructions = instructionsFor(router);\n\n const wrapper = new McpServer(identityFor(router), { capabilities, instructions });\n const server = wrapper.server;\n\n let runId: string | undefined;\n let resolveReady: (id: string) => void = () => undefined;\n const ready = new Promise<string>((resolve) => {\n resolveReady = resolve;\n });\n\n let inflight = 0;\n const idle: (() => void)[] = [];\n const enter = (): void => {\n inflight += 1;\n };\n /**\n * Every decrement goes through here, including the one that parks a call at\n * the gate. A decrement that reached zero without waking the waiters would\n * leave a shutdown draining for ever against a counter that is already idle.\n */\n const leave = (): void => {\n inflight -= 1;\n if (inflight === 0) {\n for (const resolve of idle.splice(0)) {\n resolve();\n }\n }\n };\n const whenIdle = async (): Promise<void> => {\n if (inflight === 0) {\n return;\n }\n await new Promise<void>((resolve) => idle.push(resolve));\n };\n\n // A client that pipelines notifications/initialized ahead of the initialize\n // response can reach oninitialized before its own identity is recorded, so\n // the label is filled in at the first opportunity rather than once.\n let labelled = false;\n const ensureLabel = (): void => {\n if (labelled || runId === undefined) {\n return;\n }\n const name = server.getClientVersion()?.name;\n if (name !== undefined) {\n journal.setRunLabel(runId, name);\n labelled = true;\n }\n };\n\n const supports = (\n upstream: Upstream,\n key: keyof ServerCapabilities,\n ): boolean => upstream.client.getServerCapabilities()?.[key] !== undefined;\n\n const ask = async (\n upstream: Upstream,\n request: Request,\n signal: AbortSignal,\n ): Promise<Passthrough> => {\n try {\n return await upstream.client.request(request, PassthroughResult, {\n signal,\n });\n } catch (error: unknown) {\n return rethrow(upstream.name, request.method, error);\n }\n };\n\n // --- resource ownership -------------------------------------------------\n // A resource uri is an opaque identifier the client hands back verbatim, so\n // unlike a tool name it cannot be namespaced. Ownership therefore has to be\n // discovered from what each server advertises.\n let owners: Map<string, string> | undefined;\n let schemes: Map<string, string> | undefined;\n let conflict: string | undefined;\n\n const refreshResources = async (signal: AbortSignal): Promise<void> => {\n const nextOwners = new Map<string, string>();\n const nextSchemes = new Map<string, string>();\n let nextConflict: string | undefined;\n\n for (const upstream of router.upstreams) {\n if (!supports(upstream, \"resources\")) {\n continue;\n }\n const resources = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"resources/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n signal,\n );\n const page = ResourceList.parse(raw);\n return { items: page.resources, nextCursor: page.nextCursor };\n });\n for (const resource of resources) {\n const existing = nextOwners.get(resource.uri);\n if (existing !== undefined && existing !== upstream.name) {\n nextConflict ??= `resource ${resource.uri} is advertised by both ${existing} and ${upstream.name}; a uri cannot be namespaced, so one of them must stop exposing it`;\n }\n nextOwners.set(resource.uri, existing ?? upstream.name);\n const scheme = resource.uri.split(\":\")[0] ?? \"\";\n if (scheme !== \"\" && !nextSchemes.has(scheme)) {\n nextSchemes.set(scheme, upstream.name);\n }\n }\n\n const templates = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"resources/templates/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n signal,\n );\n const page = TemplateList.parse(raw);\n return { items: page.resourceTemplates, nextCursor: page.nextCursor };\n });\n for (const template of templates) {\n const scheme = template.uriTemplate.split(\":\")[0] ?? \"\";\n if (scheme !== \"\" && !nextSchemes.has(scheme)) {\n nextSchemes.set(scheme, upstream.name);\n }\n }\n }\n\n owners = nextOwners;\n schemes = nextSchemes;\n conflict = nextConflict;\n };\n\n const ensureResources = async (signal: AbortSignal): Promise<void> => {\n if (owners === undefined) {\n await refreshResources(signal);\n }\n if (conflict !== undefined) {\n throw new McpError(ErrorCode.InternalError, conflict);\n }\n };\n\n const ownerOf = async (\n uri: string,\n signal: AbortSignal,\n ): Promise<Upstream> => {\n await ensureResources(signal);\n const direct = owners?.get(uri);\n const scheme = uri.split(\":\")[0] ?? \"\";\n const name = direct ?? schemes?.get(scheme);\n const upstream = name === undefined ? undefined : router.byName(name);\n if (upstream === undefined) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `no configured server provides ${uri}`,\n );\n }\n return upstream;\n };\n\n // --- handlers -----------------------------------------------------------\n if (capabilities.tools !== undefined) {\n server.setRequestHandler(\n ListToolsRequestSchema,\n async (_request, extra) => {\n const tools: Passthrough[] = [];\n for (const upstream of router.upstreams) {\n if (!supports(upstream, \"tools\")) {\n continue;\n }\n const items = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"tools/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n extra.signal,\n );\n const page = ToolList.parse(raw);\n return { items: page.tools, nextCursor: page.nextCursor };\n });\n for (const tool of items) {\n tools.push({\n ...tool,\n name: router.expose(upstream.name, tool.name),\n });\n }\n }\n // Pagination is flattened: a cursor would have to encode a position\n // across several independent servers, and the client gains nothing.\n return { tools };\n },\n );\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n if (runId === undefined) {\n throw new UpstreamError(\"proxy\", \"tools/call\", \"no active run\");\n }\n // Captured: narrowing does not survive into the closures below.\n const activeRun = runId;\n ensureLabel();\n const route = router.route(request.params.name);\n if (route === undefined) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `no configured server provides tool ${request.params.name}`,\n );\n }\n\n const { policy } = policies.resolve(qualify(route.upstream.name, route.tool));\n const args = request.params.arguments ?? {};\n // Counted from here, not from the forward call: the pre-read is part of\n // the action, and a shutdown that aborts it blocks a legitimate write.\n enter();\n try {\n const wantsGate =\n policy.gate === \"always\" || (policy.gate === \"on_write\" && shouldGateOnWrite(args));\n\n // A retry after an out-of-band approval reuses the row that was\n // approved, so the approval ends up on the action that actually ran\n // rather than on an abandoned twin of it.\n const granted = wantsGate\n ? journal.findApproval({\n server: route.upstream.name,\n tool: route.tool,\n args,\n notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString(),\n })\n : undefined;\n\n // An approval granted in an earlier session cannot simply be adopted:\n // the action belongs to the run happening now, or undoing this run\n // would not include it.\n const inherited =\n granted !== undefined && granted.runId !== activeRun ? granted : undefined;\n\n // Nobody has answered yet and the agent is asking again. Reusing the\n // row it is already waiting on keeps one call to one decision, which\n // is what `synartesis gates` and `approve` both assume.\n const waiting =\n granted === undefined && wantsGate\n ? journal.findGated({\n runId: activeRun,\n server: route.upstream.name,\n tool: route.tool,\n args,\n })\n : undefined;\n\n const reusable = waiting ?? (inherited === undefined ? granted : undefined);\n const pending =\n reusable === undefined\n ? journal.recordPending({\n runId: activeRun,\n server: route.upstream.name,\n tool: route.tool,\n args,\n class: policy.class,\n })\n : {\n actionId: reusable.id,\n seq: reusable.seq,\n idempotencyKey: reusable.idempotencyKey,\n };\n\n if (inherited !== undefined) {\n journal.adoptApproval(pending.actionId, inherited);\n } else if (granted !== undefined && waiting === undefined) {\n // Reusing the approved row itself: from here its outcome stops being\n // known, so it stops being `approved`.\n journal.markInFlight(granted.id);\n }\n if (granted !== undefined) {\n log?.info(\n { action: pending.actionId, by: granted.approvedBy, from: granted.runId },\n \"proceeding on a standing approval\",\n );\n }\n\n const decide = async (why: string): Promise<void> => {\n // Parked, not working: a suspended call must not hold up shutdown,\n // and the drain exists to let real work finish.\n leave();\n let decision;\n try {\n decision = await gate.decide({\n actionId: pending.actionId,\n runId: activeRun,\n seq: pending.seq,\n server: route.upstream.name,\n tool: route.tool,\n args,\n why,\n signal: extra.signal,\n });\n } finally {\n enter();\n }\n log?.info(\n { action: pending.actionId, approved: decision.approved },\n decision.approved ? \"approved\" : \"denied\",\n );\n // An approval that lands after the client has given up would send\n // a real email that the agent has already reported as not sent.\n // Nobody is waiting for the result, so the safe reading of an\n // approval nobody can hear is that it did not happen.\n if (decision.approved && extra.signal.aborted) {\n journal.settleAsDenied(\n pending.actionId,\n decision.by,\n \"approved, but the client had already stopped waiting, so it was not sent\",\n );\n throw new McpError(\n ErrorCode.InvalidRequest,\n `synartesis blocked ${request.params.name}: it was approved after the client stopped waiting, so it was not sent. Ask the agent to try again.`,\n );\n }\n if (!decision.approved) {\n if (decision.awaiting === true) {\n log?.warn(\n {\n action: pending.actionId,\n tool: `${route.upstream.name}.${route.tool}`,\n approve: options.approveHint?.(pending.actionId) ?? pending.actionId,\n },\n \"awaiting approval\",\n );\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Synartesis is holding this call for approval, because ${why}. ${decision.reason}`,\n );\n }\n const who = decision.by === undefined ? \"\" : ` by ${decision.by}`;\n throw new McpError(\n ErrorCode.InvalidRequest,\n `synartesis blocked ${request.params.name}: ${why} and was denied${who}. ${decision.reason}`,\n );\n }\n };\n\n // D4/3.4: a policy gate suspends before anything is read or written, so\n // a gated action never even looks at the resource.\n // decide() throws on refusal, so getting past this means approved.\n const askedAlready = wantsGate;\n if (wantsGate && granted === undefined) {\n await decide(\"this action cannot be undone\");\n }\n\n // The pre-read happens before the write goes out, and a failure stops\n // the write entirely: a reversible action without a snapshot is\n // silently irreversible, which is worse than the action not happening.\n let snapshot: unknown;\n let verify: ResolvedRead | undefined;\n let missingPriorState: string | undefined;\n if (policy.snapshot !== undefined) {\n try {\n verify = planRead(policy.snapshot, { args });\n snapshot = await runRead(router, verify, extra.signal);\n journal.attachSnapshot(pending.actionId, snapshot);\n } catch (error: unknown) {\n const reason = describe(error);\n if (error instanceof SnapshotError && error.absent) {\n // Nothing exists here yet, so this call creates rather than\n // replaces and there is nothing to put back. It is an\n // irreversible action wearing a reversible policy. Refusing\n // outright would mean an agent could never create anything, so\n // it falls through to the same question the gate asks.\n missingPriorState = reason;\n verify = undefined;\n } else {\n journal.markFailed(pending.actionId, reason);\n log?.error(\n { seq: pending.seq, tool: route.tool, reason },\n \"write blocked: snapshot failed\",\n );\n throw new McpError(\n ErrorCode.InternalError,\n `synartesis blocked ${request.params.name}: ${reason}`,\n );\n }\n }\n }\n\n if (missingPriorState !== undefined && !askedAlready) {\n // An approval granted out of band counts here too. It was only ever\n // looked up for a policy that asked to be gated, so a write whose\n // prior state was missing -- an agent creating a file, the commonest\n // thing an agent does -- asked, was approved, and asked again, and\n // no number of approvals ever let it through. The instructions this\n // proxy sends to every agent promise the opposite.\n const standing = journal.findApproval({\n server: route.upstream.name,\n tool: route.tool,\n args,\n notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString(),\n });\n if (standing === undefined) {\n // Not \"nothing exists here\": every tool-level error on a pre-read\n // arrives here, so a file that exists and merely could not be read\n // came out as one that was not there. The person approving an\n // unundoable write was shown absence and given no way to learn\n // otherwise until after they had allowed it. Say what happened and\n // hand over the server's own words.\n await decide(\n `nothing was captured to restore, so this cannot be undone — the read said: ${missingPriorState}`,\n );\n } else {\n // Moved onto the row that actually runs, which also spends it: an\n // approval answers one call, not every call that looks like it.\n journal.adoptApproval(pending.actionId, standing);\n log?.info(\n { action: pending.actionId, by: standing.approvedBy, from: standing.runId },\n \"proceeding on a standing approval\",\n );\n }\n }\n\n const forwarded: Request = {\n method: \"tools/call\",\n params: { ...request.params, name: route.tool },\n };\n\n try {\n const result = await route.upstream.client.request(forwarded, PassthroughResult, {\n signal: extra.signal,\n });\n\n // The server understood the call and did not do it. Recording that\n // as an action would be worse than not recording it at all: an\n // inverse resolved from a refusal is a compensating call for\n // something that never happened, and undo would faithfully carry it\n // out. The agent still sees the refusal exactly as sent.\n const refused = refusal(result);\n if (refused !== undefined) {\n journal.markFailed(pending.actionId, `the upstream refused the call: ${refused}`);\n log?.debug(\n { seq: pending.seq, tool: route.tool, reason: refused },\n \"refused by the upstream\",\n );\n return result;\n }\n\n const context = { args, snapshot, result: toPayload(result) };\n const warnings: string[] = [];\n if (missingPriorState !== undefined) {\n warnings.push(\n `no prior state existed, so there is nothing to restore: ${missingPriorState}`,\n );\n }\n\n // Resolved now rather than at rollback time (D5).\n let inverse: unknown;\n if (policy.inverse !== undefined && missingPriorState === undefined) {\n try {\n inverse = planInverse(policy.inverse, context);\n } catch (error: unknown) {\n warnings.push(`inverse could not be resolved: ${describe(error)}`);\n }\n }\n\n // Best effort: the write has already applied, so a failed post-read\n // cannot undo it. Phase 4 fails closed when the post-state is\n // missing. A resource that is now absent is a captured post-state,\n // not a missing one.\n let postSnapshot: unknown;\n if (verify !== undefined) {\n try {\n postSnapshot = await observeState(router, verify, extra.signal);\n } catch (error: unknown) {\n warnings.push(`post-state could not be captured: ${describe(error)}`);\n }\n }\n\n if (warnings.length > 0) {\n log?.warn({ seq: pending.seq, tool: route.tool, warnings }, \"applied with reservations\");\n }\n log?.debug(\n { seq: pending.seq, server: route.upstream.name, tool: route.tool, class: policy.class },\n \"applied\",\n );\n journal.markApplied(pending.actionId, {\n result,\n ...(inverse === undefined ? {} : { inverse }),\n ...(verify === undefined ? {} : { verify }),\n ...(postSnapshot === undefined ? {} : { postSnapshot }),\n ...(warnings.length === 0 ? {} : { warning: warnings.join(\"; \") }),\n });\n return result;\n } catch (error: unknown) {\n const disconnected = isDisconnected(error);\n if (extra.signal.aborted || mayHaveArrived(error)) {\n // A transport that closed while a reply was still owed says\n // nothing about whether the call arrived. Recording that as failed\n // asserts it did not, and undo would then step over an action that\n // may well have applied. Having had no connection to write to at\n // all is the other case, and that one really did not happen.\n journal.markUnknown(pending.actionId, describe(error));\n } else {\n journal.markFailed(pending.actionId, describe(error));\n }\n if (disconnected && route.upstream.reconnect !== undefined) {\n // Not to retry this call -- a write must never be sent twice on a\n // guess -- but so the rest of the session is not lost with it.\n await route.upstream.reconnect().catch(() => undefined);\n }\n return rethrow(route.upstream.name, \"tools/call\", error);\n }\n } finally {\n leave();\n }\n });\n }\n\n if (capabilities.resources !== undefined) {\n server.setRequestHandler(\n ListResourcesRequestSchema,\n async (_request, extra) => {\n await refreshResources(extra.signal);\n await ensureResources(extra.signal);\n const resources: Passthrough[] = [];\n for (const upstream of router.upstreams) {\n if (!supports(upstream, \"resources\")) {\n continue;\n }\n const items = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"resources/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n extra.signal,\n );\n const page = ResourceList.parse(raw);\n return { items: page.resources, nextCursor: page.nextCursor };\n });\n resources.push(...items);\n }\n return { resources };\n },\n );\n\n server.setRequestHandler(\n ListResourceTemplatesRequestSchema,\n async (_request, extra) => {\n const resourceTemplates: Passthrough[] = [];\n for (const upstream of router.upstreams) {\n if (!supports(upstream, \"resources\")) {\n continue;\n }\n const items = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"resources/templates/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n extra.signal,\n );\n const page = TemplateList.parse(raw);\n return {\n items: page.resourceTemplates,\n nextCursor: page.nextCursor,\n };\n });\n resourceTemplates.push(...items);\n }\n return { resourceTemplates };\n },\n );\n\n server.setRequestHandler(\n ReadResourceRequestSchema,\n async (request, extra) => {\n const upstream = await ownerOf(request.params.uri, extra.signal);\n return ask(upstream, request, extra.signal);\n },\n );\n\n if (capabilities.resources.subscribe === true) {\n for (const schema of [SubscribeRequestSchema, UnsubscribeRequestSchema]) {\n server.setRequestHandler(schema, async (request, extra) => {\n const upstream = await ownerOf(request.params.uri, extra.signal);\n return ask(upstream, request, extra.signal);\n });\n }\n }\n }\n\n if (capabilities.prompts !== undefined) {\n server.setRequestHandler(\n ListPromptsRequestSchema,\n async (_request, extra) => {\n const prompts: Passthrough[] = [];\n for (const upstream of router.upstreams) {\n if (!supports(upstream, \"prompts\")) {\n continue;\n }\n const items = await drain(async (cursor) => {\n const raw = await ask(\n upstream,\n {\n method: \"prompts/list\",\n params: cursor === undefined ? {} : { cursor },\n },\n extra.signal,\n );\n const page = PromptList.parse(raw);\n return { items: page.prompts, nextCursor: page.nextCursor };\n });\n for (const prompt of items) {\n prompts.push({\n ...prompt,\n name: router.expose(upstream.name, prompt.name),\n });\n }\n }\n return { prompts };\n },\n );\n\n server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => {\n const route = router.route(request.params.name);\n if (route === undefined) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `no configured server provides prompt ${request.params.name}`,\n );\n }\n return ask(\n route.upstream,\n {\n method: \"prompts/get\",\n params: { ...request.params, name: route.tool },\n },\n extra.signal,\n );\n });\n }\n\n if (capabilities.completions !== undefined) {\n server.setRequestHandler(CompleteRequestSchema, async (request, extra) => {\n const reference = request.params.ref;\n if (reference.type === \"ref/prompt\") {\n const route = router.route(reference.name);\n if (route === undefined) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `unknown prompt ${reference.name}`,\n );\n }\n return ask(\n route.upstream,\n {\n method: \"completion/complete\",\n params: {\n ...request.params,\n ref: { ...reference, name: route.tool },\n },\n },\n extra.signal,\n );\n }\n const upstream = await ownerOf(reference.uri, extra.signal);\n return ask(upstream, request, extra.signal);\n });\n }\n\n if (capabilities.logging !== undefined) {\n server.setRequestHandler(SetLevelRequestSchema, async (request, extra) => {\n // Broadcast: the client is configuring one logical server.\n for (const upstream of router.upstreams) {\n if (supports(upstream, \"logging\")) {\n await ask(upstream, request, extra.signal);\n }\n }\n return {};\n });\n }\n\n // --- lifecycle ----------------------------------------------------------\n let connected = false;\n for (const upstream of router.upstreams) {\n upstream.client.fallbackNotificationHandler = async (\n notification,\n ): Promise<void> => {\n if (notification.method.endsWith(\"list_changed\")) {\n owners = undefined;\n schemes = undefined;\n conflict = undefined;\n }\n if (connected) {\n await server.notification(notification);\n }\n };\n }\n\n server.oninitialized = (): void => {\n connected = true;\n const name = server.getClientVersion()?.name;\n const id = journal.beginRun(name);\n runId = id;\n labelled = name !== undefined;\n resolveReady(id);\n };\n\n const previousOnClose = server.onclose;\n server.onclose = (): void => {\n connected = false;\n if (runId !== undefined) {\n journal.endRun(runId, \"complete\");\n runId = undefined;\n }\n previousOnClose?.();\n };\n\n return {\n server: wrapper,\n ready,\n whenIdle,\n busy: (): boolean => inflight > 0,\n get runId(): string | undefined {\n return runId;\n },\n };\n}\n","/**\n * The `on_write` heuristic for tools whose destructiveness cannot be decided\n * statically, such as a raw SQL runner.\n *\n * This is a heuristic and is documented as one. It exists because the\n * alternative for `postgres.query` is to gate every SELECT, which no operator\n * would tolerate for long. Anything it cannot confidently read as a read is\n * gated (D4): failing to recognise a statement is not evidence that it is safe.\n * `always` remains the correct choice wherever certainty matters.\n */\nconst READ_ONLY = /^(select|with|show|explain|describe|desc|values|table)\\b/;\n\nfunction isReadOnlyStatement(text: string): boolean {\n const stripped = text\n .replace(/--[^\\n]*/g, \" \")\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \" \")\n .trim();\n if (!READ_ONLY.test(stripped.toLowerCase())) {\n return false;\n }\n // More than one statement means the leading SELECT says nothing about what\n // follows it.\n return stripped.replace(/;\\s*$/, \"\").indexOf(\";\") === -1;\n}\n\nexport function shouldGateOnWrite(args: unknown): boolean {\n if (typeof args !== \"object\" || args === null) {\n return true;\n }\n const strings = Object.values(args).filter(\n (value): value is string => typeof value === \"string\",\n );\n if (strings.length === 0) {\n return true;\n }\n return !strings.every(isReadOnlyStatement);\n}\n"],"mappings":"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","names":["upstream","resolve","resolve"]}
|
package/dist/toy-crm.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// fixtures/toy-crm/stdio.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z as z2 } from "zod";
|
|
8
|
+
|
|
9
|
+
// fixtures/toy-crm/server.ts
|
|
10
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
// fixtures/toy-crm/store.ts
|
|
14
|
+
var PLANS = ["free", "pro", "enterprise"];
|
|
15
|
+
var CustomerNotFoundError = class extends Error {
|
|
16
|
+
constructor(customerId) {
|
|
17
|
+
super(`no customer with id ${customerId}`);
|
|
18
|
+
this.customerId = customerId;
|
|
19
|
+
this.name = "CustomerNotFoundError";
|
|
20
|
+
}
|
|
21
|
+
customerId;
|
|
22
|
+
};
|
|
23
|
+
var SEED = [
|
|
24
|
+
{ id: "c_001", name: "Ada Lovelace", email: "ada@example.com", plan: "pro", notes: "founding customer" },
|
|
25
|
+
{ id: "c_002", name: "Grace Hopper", email: "grace@example.com", plan: "enterprise", notes: "renewal in March" },
|
|
26
|
+
{ id: "c_003", name: "Alan Turing", email: "alan@example.com", plan: "free", notes: "" }
|
|
27
|
+
];
|
|
28
|
+
var ToyCrmStore = class {
|
|
29
|
+
#customers = /* @__PURE__ */ new Map();
|
|
30
|
+
#outbox = [];
|
|
31
|
+
#nextId = SEED.length + 1;
|
|
32
|
+
#now;
|
|
33
|
+
#beforeWrite;
|
|
34
|
+
#afterWrite;
|
|
35
|
+
constructor(options2 = {}) {
|
|
36
|
+
this.#now = options2.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
37
|
+
this.#beforeWrite = options2.beforeWrite ?? (() => void 0);
|
|
38
|
+
this.#afterWrite = options2.afterWrite ?? (() => void 0);
|
|
39
|
+
for (const customer of SEED) {
|
|
40
|
+
this.#customers.set(customer.id, customer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getCustomer(id) {
|
|
44
|
+
const customer = this.#customers.get(id);
|
|
45
|
+
if (customer === void 0) {
|
|
46
|
+
throw new CustomerNotFoundError(id);
|
|
47
|
+
}
|
|
48
|
+
return customer;
|
|
49
|
+
}
|
|
50
|
+
listCustomers() {
|
|
51
|
+
return [...this.#customers.keys()].sort().map((id) => this.getCustomer(id));
|
|
52
|
+
}
|
|
53
|
+
createCustomer(draft) {
|
|
54
|
+
this.#beforeWrite();
|
|
55
|
+
const id = `c_${String(this.#nextId).padStart(3, "0")}`;
|
|
56
|
+
this.#nextId += 1;
|
|
57
|
+
const customer = { id, ...draft };
|
|
58
|
+
this.#customers.set(id, customer);
|
|
59
|
+
this.#afterWrite();
|
|
60
|
+
return customer;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Restores a customer under a caller-supplied id. This is what makes
|
|
64
|
+
* delete_customer reversible; a plain create would allocate a new id and
|
|
65
|
+
* leave every foreign key pointing at nothing.
|
|
66
|
+
*/
|
|
67
|
+
restoreCustomer(customer) {
|
|
68
|
+
this.#beforeWrite();
|
|
69
|
+
this.#customers.set(customer.id, customer);
|
|
70
|
+
this.#afterWrite();
|
|
71
|
+
return customer;
|
|
72
|
+
}
|
|
73
|
+
updateCustomer(id, patch) {
|
|
74
|
+
this.#beforeWrite();
|
|
75
|
+
const current = this.getCustomer(id);
|
|
76
|
+
const updated = {
|
|
77
|
+
id: current.id,
|
|
78
|
+
name: patch.name ?? current.name,
|
|
79
|
+
email: patch.email ?? current.email,
|
|
80
|
+
plan: patch.plan ?? current.plan,
|
|
81
|
+
notes: patch.notes ?? current.notes
|
|
82
|
+
};
|
|
83
|
+
this.#customers.set(id, updated);
|
|
84
|
+
this.#afterWrite();
|
|
85
|
+
return updated;
|
|
86
|
+
}
|
|
87
|
+
deleteCustomer(id) {
|
|
88
|
+
this.#beforeWrite();
|
|
89
|
+
const customer = this.getCustomer(id);
|
|
90
|
+
this.#customers.delete(id);
|
|
91
|
+
this.#afterWrite();
|
|
92
|
+
return customer;
|
|
93
|
+
}
|
|
94
|
+
sendEmail(to, subject, body) {
|
|
95
|
+
this.#beforeWrite();
|
|
96
|
+
const email = { to, subject, body, sentAt: this.#now() };
|
|
97
|
+
this.#outbox = [...this.#outbox, email];
|
|
98
|
+
this.#afterWrite();
|
|
99
|
+
return email;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Replaces the whole store. Used to hydrate a persisted fixture so that a
|
|
103
|
+
* rollback run in a separate process sees the state the agent left behind.
|
|
104
|
+
*/
|
|
105
|
+
__restore(state) {
|
|
106
|
+
this.#customers.clear();
|
|
107
|
+
let highest = 0;
|
|
108
|
+
for (const [id, customer] of Object.entries(state.customers)) {
|
|
109
|
+
this.#customers.set(id, { ...customer });
|
|
110
|
+
const numeric = Number(/^c_(\d+)$/.exec(id)?.[1] ?? "0");
|
|
111
|
+
highest = Math.max(highest, numeric);
|
|
112
|
+
}
|
|
113
|
+
this.#outbox = state.outbox.map((email) => ({ ...email }));
|
|
114
|
+
this.#nextId = highest + 1;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Test helper: a detached deep copy of the whole store. Keys are sorted so
|
|
118
|
+
* that two structurally equal stores also serialise identically, which is
|
|
119
|
+
* what the Phase 4 rollback assertion actually compares.
|
|
120
|
+
*/
|
|
121
|
+
__snapshot() {
|
|
122
|
+
const ids = [...this.#customers.keys()].sort();
|
|
123
|
+
return {
|
|
124
|
+
customers: Object.fromEntries(ids.map((id) => [id, { ...this.getCustomer(id) }])),
|
|
125
|
+
outbox: this.#outbox.map((email) => ({ ...email }))
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// fixtures/toy-crm/server.ts
|
|
131
|
+
var planSchema = z.enum(PLANS);
|
|
132
|
+
function json(value) {
|
|
133
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
134
|
+
}
|
|
135
|
+
function toolError(message) {
|
|
136
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
137
|
+
}
|
|
138
|
+
function guard(run) {
|
|
139
|
+
try {
|
|
140
|
+
return run();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof CustomerNotFoundError) {
|
|
143
|
+
return toolError(error.message);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function createToyCrmServer(store2) {
|
|
149
|
+
const server2 = new McpServer(
|
|
150
|
+
{ name: "toy-crm", version: "1.0.0" },
|
|
151
|
+
{
|
|
152
|
+
capabilities: { tools: {}, resources: {} },
|
|
153
|
+
instructions: "A toy CRM. Customer ids look like c_001."
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
server2.registerResource(
|
|
157
|
+
"customers",
|
|
158
|
+
"crm://customers",
|
|
159
|
+
{ description: "Every customer record.", mimeType: "application/json" },
|
|
160
|
+
(uri) => ({
|
|
161
|
+
contents: [
|
|
162
|
+
{
|
|
163
|
+
uri: uri.href,
|
|
164
|
+
mimeType: "application/json",
|
|
165
|
+
text: JSON.stringify(store2.listCustomers())
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
server2.registerResource(
|
|
171
|
+
"customer",
|
|
172
|
+
new ResourceTemplate("crm://customers/{id}", { list: void 0 }),
|
|
173
|
+
{ description: "A single customer record.", mimeType: "application/json" },
|
|
174
|
+
(uri, { id }) => ({
|
|
175
|
+
contents: [
|
|
176
|
+
{
|
|
177
|
+
uri: uri.href,
|
|
178
|
+
mimeType: "application/json",
|
|
179
|
+
text: JSON.stringify(store2.getCustomer(typeof id === "string" ? id : ""))
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
server2.registerTool(
|
|
185
|
+
"get_customer",
|
|
186
|
+
{
|
|
187
|
+
description: "Fetch a single customer record by id.",
|
|
188
|
+
inputSchema: { id: z.string().describe("Customer id, for example c_001.") },
|
|
189
|
+
annotations: { readOnlyHint: true }
|
|
190
|
+
},
|
|
191
|
+
({ id }) => guard(() => json(store2.getCustomer(id)))
|
|
192
|
+
);
|
|
193
|
+
server2.registerTool(
|
|
194
|
+
"create_customer",
|
|
195
|
+
{
|
|
196
|
+
description: "Create a customer and return the created record, including its assigned id.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
name: z.string(),
|
|
199
|
+
email: z.string(),
|
|
200
|
+
plan: planSchema.default("free"),
|
|
201
|
+
notes: z.string().default("")
|
|
202
|
+
},
|
|
203
|
+
annotations: { destructiveHint: false }
|
|
204
|
+
},
|
|
205
|
+
(draft) => json(store2.createCustomer(draft))
|
|
206
|
+
);
|
|
207
|
+
server2.registerTool(
|
|
208
|
+
"update_customer",
|
|
209
|
+
{
|
|
210
|
+
description: "Apply a partial patch to a customer. Omitted fields are left unchanged.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
id: z.string(),
|
|
213
|
+
name: z.string().optional(),
|
|
214
|
+
email: z.string().optional(),
|
|
215
|
+
plan: planSchema.optional(),
|
|
216
|
+
notes: z.string().optional()
|
|
217
|
+
},
|
|
218
|
+
annotations: { destructiveHint: true, idempotentHint: true }
|
|
219
|
+
},
|
|
220
|
+
({ id, ...patch }) => guard(() => json(store2.updateCustomer(id, patch)))
|
|
221
|
+
);
|
|
222
|
+
server2.registerTool(
|
|
223
|
+
"delete_customer",
|
|
224
|
+
{
|
|
225
|
+
description: "Delete a customer and return the record as it was immediately before deletion.",
|
|
226
|
+
inputSchema: { id: z.string() },
|
|
227
|
+
annotations: { destructiveHint: true }
|
|
228
|
+
},
|
|
229
|
+
({ id }) => guard(() => json(store2.deleteCustomer(id)))
|
|
230
|
+
);
|
|
231
|
+
server2.registerTool(
|
|
232
|
+
"restore_customer",
|
|
233
|
+
{
|
|
234
|
+
description: "Recreate a previously deleted customer under its original id. Intended as the inverse of delete_customer.",
|
|
235
|
+
inputSchema: {
|
|
236
|
+
id: z.string(),
|
|
237
|
+
name: z.string(),
|
|
238
|
+
email: z.string(),
|
|
239
|
+
plan: planSchema,
|
|
240
|
+
notes: z.string()
|
|
241
|
+
},
|
|
242
|
+
annotations: { destructiveHint: false, idempotentHint: true }
|
|
243
|
+
},
|
|
244
|
+
(customer) => json(store2.restoreCustomer(customer))
|
|
245
|
+
);
|
|
246
|
+
server2.registerTool(
|
|
247
|
+
"send_email",
|
|
248
|
+
{
|
|
249
|
+
description: "Send an email to a customer. This cannot be recalled once sent.",
|
|
250
|
+
inputSchema: { to: z.string(), subject: z.string(), body: z.string() },
|
|
251
|
+
annotations: { destructiveHint: true, openWorldHint: true }
|
|
252
|
+
},
|
|
253
|
+
({ to, subject, body }) => json(store2.sendEmail(to, subject, body))
|
|
254
|
+
);
|
|
255
|
+
return server2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// fixtures/toy-crm/stdio.ts
|
|
259
|
+
var stateSchema = z2.object({
|
|
260
|
+
customers: z2.record(
|
|
261
|
+
z2.string(),
|
|
262
|
+
z2.object({
|
|
263
|
+
id: z2.string(),
|
|
264
|
+
name: z2.string(),
|
|
265
|
+
email: z2.string(),
|
|
266
|
+
plan: z2.enum(PLANS),
|
|
267
|
+
notes: z2.string()
|
|
268
|
+
})
|
|
269
|
+
),
|
|
270
|
+
outbox: z2.array(
|
|
271
|
+
z2.object({ to: z2.string(), subject: z2.string(), body: z2.string(), sentAt: z2.string() })
|
|
272
|
+
)
|
|
273
|
+
});
|
|
274
|
+
var at = process.argv.indexOf("--state");
|
|
275
|
+
var statePath = at === -1 ? void 0 : process.argv[at + 1];
|
|
276
|
+
var options = {};
|
|
277
|
+
var store = new ToyCrmStore(
|
|
278
|
+
statePath === void 0 ? options : {
|
|
279
|
+
afterWrite: () => {
|
|
280
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
281
|
+
writeFileSync(statePath, JSON.stringify(store.__snapshot(), null, 2));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
if (statePath !== void 0 && existsSync(statePath)) {
|
|
286
|
+
store.__restore(stateSchema.parse(JSON.parse(readFileSync(statePath, "utf8"))));
|
|
287
|
+
}
|
|
288
|
+
var server = createToyCrmServer(store);
|
|
289
|
+
process.stdin.on("end", () => {
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
await server.connect(new StdioServerTransport());
|
|
293
|
+
//# sourceMappingURL=toy-crm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../fixtures/toy-crm/stdio.ts","../fixtures/toy-crm/server.ts","../fixtures/toy-crm/store.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nimport { createToyCrmServer } from \"./server.js\";\nimport { PLANS, ToyCrmStore, type ToyCrmOptions } from \"./store.js\";\n\n/**\n * Runs the fixture as a real stdio MCP server so the proxy can spawn it the\n * same way it will spawn a production server.\n *\n * toy-crm [--state <path>]\n *\n * Without --state the store lives only for the life of the process. With it,\n * state survives across processes, which is what makes a cross-process undo\n * demonstrable at all: the rollback runs long after the agent's proxy exited.\n */\nconst stateSchema = z.object({\n customers: z.record(\n z.string(),\n z.object({\n id: z.string(),\n name: z.string(),\n email: z.string(),\n plan: z.enum(PLANS),\n notes: z.string(),\n }),\n ),\n outbox: z.array(\n z.object({ to: z.string(), subject: z.string(), body: z.string(), sentAt: z.string() }),\n ),\n});\n\nconst at = process.argv.indexOf(\"--state\");\nconst statePath = at === -1 ? undefined : process.argv[at + 1];\n\nconst options: ToyCrmOptions = {};\nconst store = new ToyCrmStore(\n statePath === undefined\n ? options\n : {\n afterWrite: (): void => {\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, JSON.stringify(store.__snapshot(), null, 2));\n },\n },\n);\n\nif (statePath !== undefined && existsSync(statePath)) {\n store.__restore(stateSchema.parse(JSON.parse(readFileSync(statePath, \"utf8\"))));\n}\n\nconst server = createToyCrmServer(store);\n\n// Exit when the pipe closes; the SDK's stdio transport does not do this for\n// us, and a fixture that outlives its client makes tests hang.\nprocess.stdin.on(\"end\", () => {\n process.exit(0);\n});\n\nawait server.connect(new StdioServerTransport());\n","import { McpServer, ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { CustomerNotFoundError, PLANS, ToyCrmStore } from \"./store.js\";\n\n/**\n * Tools are named without a server prefix, the way a real upstream MCP server\n * names them. The `crm.` prefix used in manifest matches is applied by the\n * proxy from the server's key in the manifest, not by the server itself.\n */\n\nconst planSchema = z.enum(PLANS);\n\n/** Real MCP servers overwhelmingly return JSON inside a text block; match that. */\nfunction json(value: unknown): CallToolResult {\n return { content: [{ type: \"text\", text: JSON.stringify(value) }] };\n}\n\nfunction toolError(message: string): CallToolResult {\n return { content: [{ type: \"text\", text: message }], isError: true };\n}\n\n/**\n * A missing customer is a tool-level error, not a protocol error: the agent\n * should be able to read it and recover, exactly as the gate's denial path\n * must behave in Phase 5.\n */\nfunction guard(run: () => CallToolResult): CallToolResult {\n try {\n return run();\n } catch (error: unknown) {\n if (error instanceof CustomerNotFoundError) {\n return toolError(error.message);\n }\n throw error;\n }\n}\n\nexport function createToyCrmServer(store: ToyCrmStore): McpServer {\n const server = new McpServer(\n { name: \"toy-crm\", version: \"1.0.0\" },\n {\n capabilities: { tools: {}, resources: {} },\n instructions: \"A toy CRM. Customer ids look like c_001.\",\n },\n );\n\n // Resources exist so that the proxy's resources/* passthrough is testable;\n // a tools-only fixture could not exercise it.\n server.registerResource(\n \"customers\",\n \"crm://customers\",\n { description: \"Every customer record.\", mimeType: \"application/json\" },\n (uri) => ({\n contents: [\n {\n uri: uri.href,\n mimeType: \"application/json\",\n text: JSON.stringify(store.listCustomers()),\n },\n ],\n }),\n );\n\n server.registerResource(\n \"customer\",\n new ResourceTemplate(\"crm://customers/{id}\", { list: undefined }),\n { description: \"A single customer record.\", mimeType: \"application/json\" },\n (uri, { id }) => ({\n contents: [\n {\n uri: uri.href,\n mimeType: \"application/json\",\n text: JSON.stringify(store.getCustomer(typeof id === \"string\" ? id : \"\")),\n },\n ],\n }),\n );\n\n server.registerTool(\n \"get_customer\",\n {\n description: \"Fetch a single customer record by id.\",\n inputSchema: { id: z.string().describe(\"Customer id, for example c_001.\") },\n annotations: { readOnlyHint: true },\n },\n ({ id }) => guard(() => json(store.getCustomer(id))),\n );\n\n server.registerTool(\n \"create_customer\",\n {\n description: \"Create a customer and return the created record, including its assigned id.\",\n inputSchema: {\n name: z.string(),\n email: z.string(),\n plan: planSchema.default(\"free\"),\n notes: z.string().default(\"\"),\n },\n annotations: { destructiveHint: false },\n },\n (draft) => json(store.createCustomer(draft)),\n );\n\n server.registerTool(\n \"update_customer\",\n {\n description: \"Apply a partial patch to a customer. Omitted fields are left unchanged.\",\n inputSchema: {\n id: z.string(),\n name: z.string().optional(),\n email: z.string().optional(),\n plan: planSchema.optional(),\n notes: z.string().optional(),\n },\n annotations: { destructiveHint: true, idempotentHint: true },\n },\n ({ id, ...patch }) => guard(() => json(store.updateCustomer(id, patch))),\n );\n\n server.registerTool(\n \"delete_customer\",\n {\n description: \"Delete a customer and return the record as it was immediately before deletion.\",\n inputSchema: { id: z.string() },\n annotations: { destructiveHint: true },\n },\n ({ id }) => guard(() => json(store.deleteCustomer(id))),\n );\n\n server.registerTool(\n \"restore_customer\",\n {\n description:\n \"Recreate a previously deleted customer under its original id. Intended as the inverse of delete_customer.\",\n inputSchema: {\n id: z.string(),\n name: z.string(),\n email: z.string(),\n plan: planSchema,\n notes: z.string(),\n },\n annotations: { destructiveHint: false, idempotentHint: true },\n },\n (customer) => json(store.restoreCustomer(customer)),\n );\n\n server.registerTool(\n \"send_email\",\n {\n description: \"Send an email to a customer. This cannot be recalled once sent.\",\n inputSchema: { to: z.string(), subject: z.string(), body: z.string() },\n annotations: { destructiveHint: true, openWorldHint: true },\n },\n ({ to, subject, body }) => json(store.sendEmail(to, subject, body)),\n );\n\n return server;\n}\n","/**\n * In-memory CRM standing in for a real external system.\n *\n * Behaviour is deterministic on purpose: Phase 4 asserts that a store is\n * byte-identical before a run and after undoing it, which is only meaningful\n * if ids and timestamps do not drift between runs.\n */\n\nexport const PLANS = [\"free\", \"pro\", \"enterprise\"] as const;\nexport type Plan = (typeof PLANS)[number];\n\nexport interface Customer {\n readonly id: string;\n readonly name: string;\n readonly email: string;\n readonly plan: Plan;\n readonly notes: string;\n}\n\nexport interface SentEmail {\n readonly to: string;\n readonly subject: string;\n readonly body: string;\n readonly sentAt: string;\n}\n\nexport interface ToyCrmState {\n readonly customers: Readonly<Record<string, Customer>>;\n readonly outbox: readonly SentEmail[];\n}\n\nexport interface CustomerDraft {\n readonly name: string;\n readonly email: string;\n readonly plan: Plan;\n readonly notes: string;\n}\n\nexport type CustomerPatch = {\n readonly [K in keyof CustomerDraft]?: CustomerDraft[K] | undefined;\n};\n\nexport interface ToyCrmOptions {\n /** Injected so tests can pin email timestamps. */\n readonly now?: () => string;\n /**\n * Called before every mutating operation. Exists so a test can inject a\n * failure at a chosen point, which is the only way to exercise an\n * interrupted rollback.\n */\n readonly beforeWrite?: () => void;\n /** Called after every mutating operation, so state can be persisted. */\n readonly afterWrite?: () => void;\n}\n\nexport class CustomerNotFoundError extends Error {\n constructor(public readonly customerId: string) {\n super(`no customer with id ${customerId}`);\n this.name = \"CustomerNotFoundError\";\n }\n}\n\nconst SEED: readonly Customer[] = [\n { id: \"c_001\", name: \"Ada Lovelace\", email: \"ada@example.com\", plan: \"pro\", notes: \"founding customer\" },\n { id: \"c_002\", name: \"Grace Hopper\", email: \"grace@example.com\", plan: \"enterprise\", notes: \"renewal in March\" },\n { id: \"c_003\", name: \"Alan Turing\", email: \"alan@example.com\", plan: \"free\", notes: \"\" },\n];\n\nexport class ToyCrmStore {\n readonly #customers = new Map<string, Customer>();\n #outbox: SentEmail[] = [];\n #nextId = SEED.length + 1;\n readonly #now: () => string;\n readonly #beforeWrite: () => void;\n readonly #afterWrite: () => void;\n\n constructor(options: ToyCrmOptions = {}) {\n this.#now = options.now ?? ((): string => new Date().toISOString());\n this.#beforeWrite = options.beforeWrite ?? ((): void => undefined);\n this.#afterWrite = options.afterWrite ?? ((): void => undefined);\n for (const customer of SEED) {\n this.#customers.set(customer.id, customer);\n }\n }\n\n getCustomer(id: string): Customer {\n const customer = this.#customers.get(id);\n if (customer === undefined) {\n throw new CustomerNotFoundError(id);\n }\n return customer;\n }\n\n listCustomers(): readonly Customer[] {\n return [...this.#customers.keys()].sort().map((id) => this.getCustomer(id));\n }\n\n createCustomer(draft: CustomerDraft): Customer {\n this.#beforeWrite();\n // Ids are never recycled: a rolled-back delete followed by a fresh create\n // must not collide with the id the rollback restored.\n const id = `c_${String(this.#nextId).padStart(3, \"0\")}`;\n this.#nextId += 1;\n const customer: Customer = { id, ...draft };\n this.#customers.set(id, customer);\n this.#afterWrite();\n return customer;\n }\n\n /**\n * Restores a customer under a caller-supplied id. This is what makes\n * delete_customer reversible; a plain create would allocate a new id and\n * leave every foreign key pointing at nothing.\n */\n restoreCustomer(customer: Customer): Customer {\n this.#beforeWrite();\n this.#customers.set(customer.id, customer);\n this.#afterWrite();\n return customer;\n }\n\n updateCustomer(id: string, patch: CustomerPatch): Customer {\n this.#beforeWrite();\n const current = this.getCustomer(id);\n // Merged field by field rather than by spread: an absent optional arrives\n // over the wire as an explicit `undefined`, and spreading that would erase\n // the current value instead of leaving it untouched.\n const updated: Customer = {\n id: current.id,\n name: patch.name ?? current.name,\n email: patch.email ?? current.email,\n plan: patch.plan ?? current.plan,\n notes: patch.notes ?? current.notes,\n };\n this.#customers.set(id, updated);\n this.#afterWrite();\n return updated;\n }\n\n deleteCustomer(id: string): Customer {\n this.#beforeWrite();\n const customer = this.getCustomer(id);\n this.#customers.delete(id);\n this.#afterWrite();\n return customer;\n }\n\n sendEmail(to: string, subject: string, body: string): SentEmail {\n this.#beforeWrite();\n const email: SentEmail = { to, subject, body, sentAt: this.#now() };\n this.#outbox = [...this.#outbox, email];\n this.#afterWrite();\n return email;\n }\n\n /**\n * Replaces the whole store. Used to hydrate a persisted fixture so that a\n * rollback run in a separate process sees the state the agent left behind.\n */\n __restore(state: ToyCrmState): void {\n this.#customers.clear();\n let highest = 0;\n for (const [id, customer] of Object.entries(state.customers)) {\n this.#customers.set(id, { ...customer });\n const numeric = Number(/^c_(\\d+)$/.exec(id)?.[1] ?? \"0\");\n highest = Math.max(highest, numeric);\n }\n this.#outbox = state.outbox.map((email) => ({ ...email }));\n // Ids are never recycled, so the counter resumes past anything restored.\n this.#nextId = highest + 1;\n }\n\n /**\n * Test helper: a detached deep copy of the whole store. Keys are sorted so\n * that two structurally equal stores also serialise identically, which is\n * what the Phase 4 rollback assertion actually compares.\n */\n __snapshot(): ToyCrmState {\n const ids = [...this.#customers.keys()].sort();\n return {\n customers: Object.fromEntries(ids.map((id) => [id, { ...this.getCustomer(id) }])),\n outbox: this.#outbox.map((email) => ({ ...email })),\n };\n }\n}\n"],"mappings":"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","names":["z","options","store","server","z"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Policy for @modelcontextprotocol/server-filesystem.
|
|
2
|
+
#
|
|
3
|
+
# Verified against version 2026.7.10 with real files. Point the args at the
|
|
4
|
+
# directory you want the agent confined to; the server refuses anything outside
|
|
5
|
+
# it, and this policy governs what it can do inside.
|
|
6
|
+
#
|
|
7
|
+
# The honest limit of this adapter: the filesystem server exposes no delete
|
|
8
|
+
# tool. Anything that brings a new path into existence therefore cannot be
|
|
9
|
+
# taken back by any call this server offers, so it is gated rather than
|
|
10
|
+
# pretended to be reversible.
|
|
11
|
+
version: 1
|
|
12
|
+
|
|
13
|
+
servers:
|
|
14
|
+
fs:
|
|
15
|
+
command: node
|
|
16
|
+
args: ["node_modules/@modelcontextprotocol/server-filesystem/dist/index.js", "."]
|
|
17
|
+
|
|
18
|
+
tools:
|
|
19
|
+
# --- reads -------------------------------------------------------------
|
|
20
|
+
- match: "fs.read_file"
|
|
21
|
+
class: readonly
|
|
22
|
+
- match: "fs.read_text_file"
|
|
23
|
+
class: readonly
|
|
24
|
+
- match: "fs.read_media_file"
|
|
25
|
+
class: readonly
|
|
26
|
+
- match: "fs.read_multiple_files"
|
|
27
|
+
class: readonly
|
|
28
|
+
- match: "fs.list_directory"
|
|
29
|
+
class: readonly
|
|
30
|
+
- match: "fs.list_directory_with_sizes"
|
|
31
|
+
class: readonly
|
|
32
|
+
- match: "fs.directory_tree"
|
|
33
|
+
class: readonly
|
|
34
|
+
- match: "fs.search_files"
|
|
35
|
+
class: readonly
|
|
36
|
+
- match: "fs.get_file_info"
|
|
37
|
+
class: readonly
|
|
38
|
+
- match: "fs.list_allowed_directories"
|
|
39
|
+
class: readonly
|
|
40
|
+
|
|
41
|
+
# --- writes ------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# Capture the file, then write it back. read_text_file returns
|
|
44
|
+
# structuredContent shaped {content: "..."}, so the text lives at
|
|
45
|
+
# $snapshot.content. This is exactly the sort of thing that is wrong until it
|
|
46
|
+
# is run against the real server: the first version of this policy said
|
|
47
|
+
# $snapshot and produced an object where a string was required.
|
|
48
|
+
#
|
|
49
|
+
# If the path does not exist yet, the pre-read finds nothing, there is no
|
|
50
|
+
# prior state to restore, and the call is gated instead. That is not a
|
|
51
|
+
# limitation being worked around; this server cannot delete, so creating a
|
|
52
|
+
# file really is irreversible.
|
|
53
|
+
- match: "fs.write_file"
|
|
54
|
+
class: reversible
|
|
55
|
+
snapshot:
|
|
56
|
+
tool: "fs.read_text_file"
|
|
57
|
+
args:
|
|
58
|
+
path: "$.path"
|
|
59
|
+
inverse:
|
|
60
|
+
tool: "fs.write_file"
|
|
61
|
+
args:
|
|
62
|
+
path: "$.path"
|
|
63
|
+
content: "$snapshot.content"
|
|
64
|
+
|
|
65
|
+
# An edit applies a diff, so the inverse restores the whole prior file rather
|
|
66
|
+
# than trying to invert the diff. Inverting a diff is only correct if nothing
|
|
67
|
+
# else touched the file, and drift detection is a better way to know that.
|
|
68
|
+
- match: "fs.edit_file"
|
|
69
|
+
class: reversible
|
|
70
|
+
snapshot:
|
|
71
|
+
tool: "fs.read_text_file"
|
|
72
|
+
args:
|
|
73
|
+
path: "$.path"
|
|
74
|
+
inverse:
|
|
75
|
+
tool: "fs.write_file"
|
|
76
|
+
args:
|
|
77
|
+
path: "$.path"
|
|
78
|
+
content: "$snapshot.content"
|
|
79
|
+
|
|
80
|
+
# Reversible from its arguments alone: no pre-read could say anything the
|
|
81
|
+
# arguments do not already. Drift cannot be checked for the same reason, so
|
|
82
|
+
# undo reports it as unverified.
|
|
83
|
+
# Only reversible when the destination did not exist. Moving onto a file
|
|
84
|
+
# that did overwrites it, and moving back afterwards restores the source and
|
|
85
|
+
# leaves nothing where the destination's contents were: undo reports success
|
|
86
|
+
# while the file it destroyed stays destroyed. There is no pre-read that
|
|
87
|
+
# would tell the two cases apart -- a snapshot that finds nothing is how this
|
|
88
|
+
# policy says "cannot be undone", which is backwards here, since finding
|
|
89
|
+
# nothing is the safe case. So it asks.
|
|
90
|
+
- match: "fs.move_file"
|
|
91
|
+
class: irreversible
|
|
92
|
+
gate: always
|
|
93
|
+
|
|
94
|
+
# No rmdir exists on this server, so a directory once created stays.
|
|
95
|
+
- match: "fs.create_directory"
|
|
96
|
+
class: irreversible
|
|
97
|
+
gate: always
|