feat(runner): add authenticated semantic MCP bridge (#12402)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native providers can receive only the semantic operations authorized
for one run.
> - Codex ACP consumes those operations through an MCP endpoint.
> - The endpoint must be private, authenticated, bounded, and
deterministic under retries.
> - It must not advertise runner-private operations or allow callers to
replace terminal-result schemas.
> - This pull request adds a provider-neutral loopback MCP bridge with
those controls.

## Linked Issues or Issue Description

**What would you like to improve?**

The runner has a run-scoped semantic catalog and dispatcher, but the
ACPX runtime needs a secure transport for that catalog. A generic local
MCP server could expose extra operations, accept ambiguous tool
definitions, or execute the same call twice after a retry.

**Why is this important?**

Semantic tool presence is part of the authorization boundary.
Undiscoverable operations must remain unavailable. Terminal completion
and blocked-result schemas must not be replaceable. Duplicate call
identities must be idempotent, and conflicting duplicates must fail
closed.

**Suggested approach**

Bind one MCP endpoint to `127.0.0.1` for each admitted runtime. Require
a random bearer secret. Compile the closed tool schemas before
listening. Keep private operations out of `tools/list`. Validate and
fingerprint each call before dispatch. Bound request size, result size,
time, and retained call identities. Abort active operations on timeout,
cancellation, or bridge close.

**Additional context**

#12401 is merged. This PR does not attach the bridge to ACPX, register a
provider, or change any server or direct-adapter behavior.

## What Changed

- Add a provider-neutral runner semantic MCP bridge bound only to IPv4
loopback.
- Require constant-time bearer authentication before MCP operations.
- Expose only the supplied public catalog plus fixed completion and
blocked-result tools.
- Keep runner-private operations callable by trusted extensions but
absent from discovery.
- Reject invalid names, duplicate definitions, public/private
collisions, and terminal schema replacement.
- Compile JSON Schema validators before accepting traffic.
- Validate calls before dispatch and replay identical duplicate
identities exactly once.
- Treat numeric and string JSON-RPC identities as distinct and reject
conflicting duplicates.
- Bound request bodies, result text, timeouts, retained identities, and
concurrent capacity.
- Terminate oversized request bodies, preserve successful mutation
outcomes, and keep complete semantic results.
- Propagate MCP cancellation and abort active calls during close.

## Verification

- Exact verified head: `e5070e235448680e480e8d3f66bb46ac62d71c8e`.
- Full GitHub PR workflow passed in [run
33342776925](https://github.com/paperclipai/paperclip/actions/runs/33342776925),
including runner verification/build, typecheck, all test shards, canary,
and e2e.
- Greptile is 5/5 on the exact head with zero unresolved review threads.
- Superagent Security, Snyk, contributor trust, and commitperclip passed
on the exact head.
- Storybook skipped by path as expected.
- The diff contains 2 files and does not change dependencies,
`pnpm-lock.yaml`, workflows, migrations, server selection, or UI
behavior.
- No additional local suite was run during the final restack; GitHub
Actions is the authoritative verification environment.

## Risks

The main risk is widening model-visible authority. The bridge exposes
only its closed public catalog and fixed terminal tools; private
operations are omitted from discovery and catalog ambiguity fails during
startup. Another risk is duplicate execution after a provider retry. The
bridge fingerprints each admitted JSON-RPC identity, reuses the first
promise for exact retries, and rejects changed payloads. The endpoint
uses loopback plus a per-runtime bearer secret and has no production
caller in this pull request.

## Model Used

OpenAI Codex with GPT-5 and repository tool use.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with version and capability
details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked an existing public item or described the
issue in this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal task
identifier
- [x] I have added or updated tests where applicable
- [x] I have documented the authorization, network, idempotency, and
rollout risks
- [x] All applicable GitHub Actions are green
- [x] Greptile is 5/5 with every actionable comment resolved
- [x] I have addressed all review findings before merge
This commit is contained in:
Dotta 2026-08-30 19:02:58 -05:00 committed by GitHub
parent db52ec0ca0
commit 9036d3c484
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 1298 additions and 0 deletions

View File

@ -0,0 +1,447 @@
import { connect } from "node:net";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
canonicalRunnerToolName,
startRunnerToolBridge,
type RunnerToolBridge,
} from "./runner-tool-bridge.js";
const bridges: RunnerToolBridge[] = [];
afterEach(async () => {
await Promise.all(bridges.splice(0).map((bridge) => bridge.close()));
});
async function rpc(
bridge: RunnerToolBridge,
body: Record<string, unknown>,
secret = bridge.secret,
): Promise<Response> {
return fetch(bridge.url, {
method: "POST",
headers: {
Authorization: `Bearer ${secret}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ jsonrpc: "2.0", ...body }),
});
}
describe("runner semantic MCP bridge", () => {
it("binds to loopback, requires authentication, and exposes a closed catalog", async () => {
const bridge = await startRunnerToolBridge({
secret: "session-secret",
tools: [tool("documents.read")],
handler: async () => ({ ok: true }),
});
bridges.push(bridge);
expect(new URL(bridge.url).hostname).toBe("127.0.0.1");
expect(
(await rpc(bridge, { id: 1, method: "tools/list" }, "wrong")).status,
).toBe(401);
expect(
await (await rpc(bridge, { id: 2, method: "tools/list" })).json(),
).toMatchObject({
result: {
tools: [
{ name: "documents.read" },
{ name: "paperclip_finish" },
{ name: "paperclip_block" },
],
},
});
});
it("keeps private operations callable but undiscoverable", async () => {
const handler = vi.fn(async ({ tool: name }) => ({ name }));
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
privateTools: [tool("__paperclip_permission")],
handler,
});
bridges.push(bridge);
const listed = (await (
await rpc(bridge, { id: 1, method: "tools/list" })
).json()) as { result: { tools: Array<{ name: string }> } };
expect(listed.result.tools.map(({ name }) => name)).not.toContain(
"__paperclip_permission",
);
expect(
await (
await rpc(bridge, {
id: "private-1",
method: "tools/call",
params: { name: "__paperclip_permission", arguments: {} },
})
).json(),
).toMatchObject({
result: {
content: [{ text: expect.stringContaining("__paperclip_permission") }],
},
});
});
it("normalizes names and executes identical duplicate calls once", async () => {
const handler = vi.fn(async () => ({ value: 7 }));
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
handler,
});
bridges.push(bridge);
const request = {
id: "call-1",
method: "tools/call",
params: {
name: "paperclip_documents.read",
arguments: { id: "doc" },
},
};
expect(await (await rpc(bridge, request)).json()).toMatchObject({
result: { content: [{ text: '{"value":7}' }] },
});
expect(await (await rpc(bridge, request)).json()).toMatchObject({
result: { content: [{ text: '{"value":7}' }] },
});
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
tool: "documents.read",
callId: "call-1",
arguments: { id: "doc" },
}),
);
});
it("rejects conflicting duplicate identities and validates before dispatch", async () => {
const handler = vi.fn(async () => ({ ok: true }));
const bridge = await startRunnerToolBridge({
tools: [
{
name: "documents.read",
inputSchema: {
type: "object",
required: ["id"],
properties: { id: { type: "string" } },
additionalProperties: false,
},
},
],
handler,
});
bridges.push(bridge);
expect(
await (
await rpc(bridge, {
id: "same",
method: "tools/call",
params: { name: "documents.read", arguments: { id: 9 } },
})
).json(),
).toMatchObject({ result: { isError: true } });
expect(handler).not.toHaveBeenCalled();
await rpc(bridge, {
id: "same",
method: "tools/call",
params: { name: "documents.read", arguments: { id: "a" } },
});
expect(
await (
await rpc(bridge, {
id: "same",
method: "tools/call",
params: { name: "documents.read", arguments: { id: "b" } },
})
).json(),
).toMatchObject({
result: {
isError: true,
content: [{ text: "Duplicate call identity conflict." }],
},
});
});
it("keeps numeric and string JSON-RPC identities distinct", async () => {
const handler = vi.fn(async ({ callId }) => ({ callId }));
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
handler,
});
bridges.push(bridge);
await rpc(bridge, {
id: 1,
method: "tools/call",
params: { name: "documents.read", arguments: { kind: "number" } },
});
await rpc(bridge, {
id: "1",
method: "tools/call",
params: { name: "documents.read", arguments: { kind: "string" } },
});
expect(handler).toHaveBeenCalledTimes(2);
});
it("times out calls and honors MCP cancellation", async () => {
const starts: string[] = [];
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
timeoutMs: 20,
handler: ({ callId, signal }) =>
new Promise((_resolve, reject) => {
starts.push(callId);
signal.addEventListener(
"abort",
() => reject(new Error("handler aborted")),
{ once: true },
);
}),
});
bridges.push(bridge);
expect(
await (
await rpc(bridge, {
id: "slow",
method: "tools/call",
params: { name: "documents.read", arguments: {} },
})
).json(),
).toMatchObject({
result: {
isError: true,
content: [{ text: "Paperclip tool call timed out" }],
},
});
const pending = rpc(bridge, {
id: "cancel-me",
method: "tools/call",
params: { name: "documents.read", arguments: {} },
});
await vi.waitFor(() => expect(starts).toContain("cancel-me"), {
interval: 1,
timeout: 15,
});
expect(
(
await rpc(bridge, {
method: "notifications/cancelled",
params: { requestId: "cancel-me" },
})
).status,
).toBe(202);
expect(await (await pending).json()).toMatchObject({
result: {
isError: true,
content: [{ text: "Paperclip tool call cancelled" }],
},
});
});
it("preserves successful mutation identity when the result is oversized", async () => {
const result = { text: `snowman-${"☃".repeat(65 * 1024)}` };
const handler = vi.fn(async () => result);
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
handler,
});
bridges.push(bridge);
const request = {
id: "large-result",
method: "tools/call",
params: { name: "documents.read", arguments: {} },
};
const bodies = [];
for (const response of [
await rpc(bridge, request),
await rpc(bridge, request),
]) {
const body = (await response.json()) as {
result: { content: Array<{ text: string }>; isError?: boolean };
};
expect(body.result).not.toHaveProperty("isError");
const manifest = JSON.parse(body.result.content[0]!.text) as {
schema: string;
encoding: string;
chunkCount: number;
byteLength: number;
sha256: string;
};
const serialized = body.result.content
.slice(1)
.map(({ text }) => text)
.join("");
expect(manifest).toMatchObject({
schema: "paperclip.semantic_tool_result_chunks.v1",
encoding: "json",
chunkCount: body.result.content.length - 1,
byteLength: Buffer.byteLength(serialized),
});
expect(
body.result.content
.slice(1)
.every(({ text }) => Buffer.byteLength(text) <= 64 * 1024),
).toBe(true);
expect(JSON.parse(serialized)).toEqual(result);
bodies.push(body);
}
expect(bodies[1]).toEqual(bodies[0]);
expect(handler).toHaveBeenCalledTimes(1);
});
it("returns a complete tagged receipt for cyclic results without re-executing", async () => {
const cyclic: Record<string, unknown> = {};
cyclic.self = cyclic;
cyclic.revision = 9_007_199_254_740_993n;
const handler = vi.fn(async () => cyclic);
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
handler,
});
bridges.push(bridge);
const request = {
id: "cyclic-result",
method: "tools/call",
params: { name: "documents.read", arguments: {} },
};
const first = (await (await rpc(bridge, request)).json()) as {
result: { content: Array<{ text: string }>; isError?: boolean };
};
const second = (await (await rpc(bridge, request)).json()) as typeof first;
expect(first.result).not.toHaveProperty("isError");
expect(JSON.parse(first.result.content[0]!.text)).toMatchObject({
schema: "paperclip.semantic_tool_result.v1",
status: "completed",
tool: "documents.read",
callIdentitySha256: expect.stringMatching(/^[0-9a-f]{64}$/),
encoding: "paperclip.tagged_graph.v1",
result: {
root: { $ref: 1 },
nodes: [
{
id: 1,
type: "Object",
properties: [
{ key: "self", value: { $ref: 1 } },
{
key: "revision",
value: { $type: "bigint", value: "9007199254740993" },
},
],
},
],
},
});
expect(second).toEqual(first);
expect(handler).toHaveBeenCalledTimes(1);
});
it("destroys oversized and stalled request bodies before dispatch", async () => {
const handler = vi.fn(async () => ({ ok: true }));
const bridge = await startRunnerToolBridge({
tools: [tool("documents.read")],
handler,
maxBodyBytes: 64,
requestBodyTimeoutMs: 20,
});
bridges.push(bridge);
await expect(
rpc(bridge, {
id: "oversized",
method: "tools/call",
params: {
name: "documents.read",
arguments: { value: "x".repeat(128) },
},
}),
).rejects.toThrow();
const endpoint = new URL(bridge.url);
const socket = connect({
host: endpoint.hostname,
port: Number(endpoint.port),
});
const closed = new Promise<void>((resolve) =>
socket.once("close", () => resolve()),
);
socket.write(
[
"POST /mcp HTTP/1.1",
`Host: ${endpoint.host}`,
`Authorization: Bearer ${bridge.secret}`,
"Content-Type: application/json",
"Transfer-Encoding: chunked",
"",
"5",
"{",
].join("\r\n"),
);
await expect(closed).resolves.toBeUndefined();
expect(handler).not.toHaveBeenCalled();
});
it("prevents catalog ambiguity and reserved schema replacement", async () => {
await expect(
startRunnerToolBridge({
tools: [tool("documents.read"), tool("paperclip_documents.read")],
handler: async () => null,
}),
).rejects.toThrow("duplicated");
await expect(
startRunnerToolBridge({
tools: [tool("paperclip_finish")],
handler: async () => null,
}),
).rejects.toThrow("reserved by the protocol");
await expect(
startRunnerToolBridge({
tools: [tool("documents.read")],
privateTools: [tool("documents.read")],
handler: async () => null,
}),
).rejects.toThrow("both public and private");
});
it("rejects unsupported HTTP shapes and closes idempotently", async () => {
const bridge = await startRunnerToolBridge({
tools: [],
handler: async () => null,
});
bridges.push(bridge);
expect(
(
await fetch(bridge.url, {
method: "POST",
headers: { Authorization: `Bearer ${bridge.secret}` },
body: "{}",
})
).status,
).toBe(415);
await bridge.close();
await bridge.close();
bridges.splice(bridges.indexOf(bridge), 1);
});
it("normalizes every supported provider prefix", () => {
expect(canonicalRunnerToolName("paperclip_documents.read")).toBe(
"documents.read",
);
expect(canonicalRunnerToolName("paperclip__documents.read")).toBe(
"documents.read",
);
expect(canonicalRunnerToolName("paperclip.documents.read")).toBe(
"documents.read",
);
});
});
function tool(name: string): Readonly<Record<string, unknown>> {
return { name, inputSchema: { type: "object" } };
}

View File

@ -0,0 +1,851 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import {
createServer,
type IncomingMessage,
type Server,
type ServerResponse,
} from "node:http";
import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_NAME,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_NAME,
} from "../contracts/completion-result.js";
export interface RunnerToolDefinition {
name: string;
description?: string;
inputSchema: Record<string, unknown>;
}
export interface RunnerToolCall {
tool: string;
callId: string;
arguments: unknown;
signal: AbortSignal;
}
export interface RunnerToolBridgeOptions {
tools?: readonly Readonly<Record<string, unknown>>[];
/** Runner-owned operations that are callable but never model-visible. */
privateTools?: readonly Readonly<Record<string, unknown>>[];
handler(call: RunnerToolCall): Promise<unknown>;
timeoutMs?: number;
privateToolTimeoutMs?: number;
maxBodyBytes?: number;
/** Internal test seam; production bounds headers and body reads to 30 seconds. */
requestBodyTimeoutMs?: number;
secret?: string;
}
export interface RunnerToolBridge {
readonly url: string;
readonly secret: string;
close(): Promise<void>;
}
interface AdmittedCall {
fingerprint: string;
promise: Promise<RunnerToolCallResult>;
}
interface RunnerToolTextContent {
type: "text";
text: string;
}
interface RunnerToolCallResult {
content: RunnerToolTextContent[];
}
const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_PRIVATE_TOOL_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
const DEFAULT_MAX_BODY_BYTES = 1_048_576;
const DEFAULT_REQUEST_BODY_TIMEOUT_MS = 30_000;
const MAX_CALLS = 2_048;
const MAX_RESULT_CHUNK_BYTES = 64 * 1024;
const RESERVED_TOOLS: readonly RunnerToolDefinition[] = [
{
name: PRP_COMPLETION_TOOL_NAME,
description: "Return the semantic completion result.",
inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
},
{
name: PRP_BLOCK_TOOL_NAME,
description: "Return the semantic blocked result.",
inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
},
];
export function canonicalRunnerToolName(name: string): string {
const trimmed = name.trim();
if (trimmed === PRP_COMPLETION_TOOL_NAME || trimmed === PRP_BLOCK_TOOL_NAME) {
return trimmed;
}
for (const prefix of ["paperclip__", "paperclip_", "paperclip."]) {
if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
}
return trimmed;
}
/** Start one authenticated, loopback-only MCP endpoint for a closed run catalog. */
export async function startRunnerToolBridge(
options: RunnerToolBridgeOptions,
): Promise<RunnerToolBridge> {
const secret = options.secret ?? randomBytes(32).toString("base64url");
if (secret.length === 0)
throw new Error("Runner tool bridge secret is empty");
const tools = normalizeTools(options.tools ?? [], "public");
const privateTools = normalizeTools(options.privateTools ?? [], "private");
const publicNames = new Set(tools.map((tool) => tool.name));
for (const tool of privateTools) {
if (publicNames.has(tool.name)) {
throw new Error(
`Runner tool ${tool.name} cannot be both public and private`,
);
}
}
const visibleTools = [...tools, ...structuredClone(RESERVED_TOOLS)];
const admittedTools = [...visibleTools, ...privateTools];
const validators = compileValidators(admittedTools);
const calls = new Map<string, AdmittedCall>();
const controllers = new Map<string, AbortController>();
const context = {
secret,
visibleTools,
admittedTools: new Map(admittedTools.map((tool) => [tool.name, tool])),
validators,
calls,
controllers,
handler: options.handler,
timeoutMs: positiveBoundedInteger(
options.timeoutMs,
DEFAULT_TIMEOUT_MS,
1,
24 * 60 * 60 * 1_000,
"tool timeout",
),
privateToolTimeoutMs: positiveBoundedInteger(
options.privateToolTimeoutMs,
DEFAULT_PRIVATE_TOOL_TIMEOUT_MS,
1,
24 * 60 * 60 * 1_000,
"private tool timeout",
),
privateToolNames: new Set(privateTools.map((tool) => tool.name)),
maxBodyBytes: positiveBoundedInteger(
options.maxBodyBytes,
DEFAULT_MAX_BODY_BYTES,
1,
16 * 1024 * 1024,
"request size",
),
requestBodyTimeoutMs: positiveBoundedInteger(
options.requestBodyTimeoutMs,
DEFAULT_REQUEST_BODY_TIMEOUT_MS,
1,
5 * 60 * 1_000,
"request body timeout",
),
};
const server = createServer((request, response) => {
void handleRequest(request, response, context).catch(() => {
if (!response.headersSent) response.statusCode = 500;
if (!response.writableEnded) response.end();
});
});
server.requestTimeout = context.requestBodyTimeoutMs;
server.headersTimeout = context.requestBodyTimeoutMs;
server.on("clientError", (_error, socket) => socket.destroy());
await listenLoopback(server);
const address = server.address();
if (address === null || typeof address === "string") {
await closeServer(server);
throw new Error("Runner tool bridge failed to bind a loopback port");
}
let closed = false;
return Object.freeze({
url: `http://127.0.0.1:${address.port}/mcp`,
secret,
async close() {
if (closed) return;
closed = true;
for (const controller of controllers.values()) controller.abort();
await closeServer(server);
},
});
}
async function handleRequest(
request: IncomingMessage,
response: ServerResponse,
context: {
secret: string;
visibleTools: RunnerToolDefinition[];
admittedTools: Map<string, RunnerToolDefinition>;
validators: Map<string, ValidateFunction>;
calls: Map<string, AdmittedCall>;
controllers: Map<string, AbortController>;
handler: RunnerToolBridgeOptions["handler"];
timeoutMs: number;
privateToolTimeoutMs: number;
privateToolNames: ReadonlySet<string>;
maxBodyBytes: number;
requestBodyTimeoutMs: number;
},
): Promise<void> {
setSecurityHeaders(response);
if (!authorized(request.headers.authorization, context.secret)) {
response.statusCode = 401;
response.end();
return;
}
if (request.method !== "POST" || request.url !== "/mcp") {
response.statusCode = request.method === "GET" ? 405 : 404;
if (request.method === "GET") response.setHeader("Allow", "POST");
response.end();
return;
}
if (!isJsonContentType(request.headers["content-type"])) {
response.statusCode = 415;
response.end();
return;
}
let message: Record<string, unknown>;
try {
message = parseMessage(
await readBody(
request,
context.maxBodyBytes,
context.requestBodyTimeoutMs,
),
);
} catch (error) {
writeRpc(response, null, undefined, rpcError(-32700, safeError(error)));
return;
}
const id = message.id ?? null;
const method = typeof message.method === "string" ? message.method : "";
if (method === "notifications/cancelled") {
const params = isRecord(message.params) ? message.params : {};
const requestId = params.requestId;
if (typeof requestId === "string" || typeof requestId === "number") {
context.controllers.get(rpcIdKey(requestId))?.abort();
}
response.statusCode = 202;
response.end();
return;
}
if (method === "notifications/initialized") {
response.statusCode = 202;
response.end();
return;
}
if (method === "initialize") {
response.setHeader("Mcp-Session-Id", randomBytes(16).toString("hex"));
writeRpc(response, id, {
protocolVersion: "2025-03-26",
capabilities: { tools: { listChanged: false } },
serverInfo: { name: "paperclip-runner", version: "1" },
});
return;
}
if (method === "ping") {
writeRpc(response, id, {});
return;
}
if (method === "tools/list") {
writeRpc(response, id, { tools: context.visibleTools });
return;
}
if (method !== "tools/call") {
writeRpc(response, id, undefined, rpcError(-32601, "Method not found"));
return;
}
const params = isRecord(message.params) ? message.params : {};
const rawName = typeof params.name === "string" ? params.name : "";
const tool = canonicalRunnerToolName(rawName);
if (!context.admittedTools.has(tool)) {
writeToolError(response, id, "Unsupported tool.");
return;
}
if (typeof id !== "string" && typeof id !== "number") {
writeRpc(
response,
null,
undefined,
rpcError(-32600, "Tool calls require a request id"),
);
return;
}
const callId = String(id);
const callKey = rpcIdKey(id);
const args = params.arguments ?? {};
const validator = context.validators.get(tool);
if (validator === undefined || !validator(args)) {
const detail = validator?.errors
?.map(
(error) =>
`${error.instancePath || "/"} ${error.message ?? "is invalid"}`,
)
.join("; ");
writeToolError(
response,
id,
`Invalid tool input${detail ? `: ${detail}` : "."}`,
);
return;
}
const fingerprint = canonicalJson({ tool, args });
const existing = context.calls.get(callKey);
if (existing && existing.fingerprint !== fingerprint) {
writeToolError(response, id, "Duplicate call identity conflict.");
return;
}
if (!existing && context.calls.size >= MAX_CALLS) {
for (const admittedId of context.calls.keys()) {
if (context.controllers.has(admittedId)) continue;
context.calls.delete(admittedId);
break;
}
if (context.calls.size >= MAX_CALLS) {
writeToolError(response, id, "Runner tool bridge is at call capacity.");
return;
}
}
const controller = existing === undefined ? new AbortController() : undefined;
const execution: Promise<RunnerToolCallResult> =
existing?.promise ??
withCancellationAndTimeout(
Promise.resolve()
.then(() =>
context.handler({
tool,
callId,
arguments: structuredClone(args),
signal: controller!.signal,
}),
)
.then((result) => successfulToolResult(tool, callId, result)),
controller!,
context.privateToolNames.has(tool)
? context.privateToolTimeoutMs
: context.timeoutMs,
);
if (!existing) {
context.calls.set(callKey, { fingerprint, promise: execution });
context.controllers.set(callKey, controller!);
void execution
.finally(() => context.controllers.delete(callKey))
.catch(() => undefined);
}
try {
const result = await execution;
writeRpc(response, id, result);
} catch (error) {
writeToolError(response, id, safeError(error));
}
}
function normalizeTools(
input: readonly Readonly<Record<string, unknown>>[],
visibility: "public" | "private",
): RunnerToolDefinition[] {
const tools: RunnerToolDefinition[] = [];
const seen = new Set<string>();
const reservedNames = new Set(RESERVED_TOOLS.map((tool) => tool.name));
for (const raw of input) {
const name =
typeof raw.name === "string" ? canonicalRunnerToolName(raw.name) : "";
if (!/^[A-Za-z0-9_.:-]{1,256}$/.test(name)) {
throw new Error(`Runner ${visibility} tool has an invalid name`);
}
if (reservedNames.has(name)) {
throw new Error(`Runner tool ${name} is reserved by the protocol`);
}
if (seen.has(name)) {
throw new Error(`Runner ${visibility} tool ${name} is duplicated`);
}
if (!isRecord(raw.inputSchema)) {
throw new Error(`Runner tool ${name} requires an object input schema`);
}
tools.push({
name,
...(typeof raw.description === "string"
? { description: raw.description.slice(0, 4_096) }
: {}),
inputSchema: structuredClone(raw.inputSchema),
});
seen.add(name);
}
return tools;
}
function compileValidators(
tools: readonly RunnerToolDefinition[],
): Map<string, ValidateFunction> {
const ajv = new Ajv2020({ allErrors: true, strict: false });
return new Map(
tools.map((tool) => [tool.name, ajv.compile(tool.inputSchema)]),
);
}
function authorized(value: string | undefined, secret: string): boolean {
if (!value?.startsWith("Bearer ")) return false;
const supplied = Buffer.from(value.slice(7));
const expected = Buffer.from(secret);
return (
supplied.length === expected.length && timingSafeEqual(supplied, expected)
);
}
function readBody(
request: IncomingMessage,
maxBytes: number,
timeoutMs: number,
): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let size = 0;
let settled = false;
const cleanup = () => {
clearTimeout(timer);
request.off("data", onData);
request.off("end", onEnd);
request.off("error", onError);
request.off("aborted", onAborted);
};
const fail = (error: Error, destroy = false) => {
if (settled) return;
settled = true;
cleanup();
if (destroy && !request.destroyed) request.destroy();
reject(error);
};
const onData = (value: Buffer | string) => {
if (settled) return;
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
if (chunk.length > maxBytes - size) {
fail(
new Error("MCP request exceeded the retained payload limit"),
true,
);
return;
}
size += chunk.length;
chunks.push(chunk);
};
const onEnd = () => {
if (settled) return;
settled = true;
cleanup();
resolve(Buffer.concat(chunks).toString("utf8"));
};
const onError = (error: Error) => fail(error);
const onAborted = () => fail(new Error("MCP request body was aborted"));
const timer = setTimeout(() => {
fail(new Error("MCP request body timed out"), true);
}, timeoutMs);
timer.unref?.();
request.on("data", onData);
request.on("end", onEnd);
request.on("error", onError);
request.on("aborted", onAborted);
});
}
function parseMessage(body: string): Record<string, unknown> {
const parsed: unknown = JSON.parse(body);
if (
!isRecord(parsed) ||
parsed.jsonrpc !== "2.0" ||
typeof parsed.method !== "string"
) {
throw new Error("Invalid JSON-RPC request");
}
return parsed;
}
function writeToolError(
response: ServerResponse,
id: unknown,
message: string,
): void {
writeRpc(response, id, {
isError: true,
content: [{ type: "text", text: message }],
});
}
function writeRpc(
response: ServerResponse,
id: unknown,
result?: unknown,
error?: unknown,
): void {
response.statusCode = 200;
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
jsonrpc: "2.0",
id,
...(error === undefined ? { result } : { error }),
}),
);
}
function rpcError(
code: number,
message: string,
): { code: number; message: string } {
return { code, message };
}
function setSecurityHeaders(response: ServerResponse): void {
response.setHeader("Cache-Control", "no-store");
response.setHeader("X-Content-Type-Options", "nosniff");
}
function listenLoopback(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
const onError = (error: Error) => {
server.off("listening", onListening);
reject(error);
};
const onListening = () => {
server.off("error", onError);
resolve();
};
server.once("error", onError);
server.once("listening", onListening);
server.listen(0, "127.0.0.1");
});
}
function closeServer(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
server.closeIdleConnections();
});
}
function withCancellationAndTimeout<T>(
promise: Promise<T>,
controller: AbortController,
timeoutMs: number,
): Promise<T> {
return new Promise((resolve, reject) => {
let settled = false;
const finish = (callback: () => void) => {
if (settled) return;
settled = true;
clearTimeout(timer);
controller.signal.removeEventListener("abort", onAbort);
callback();
};
const onAbort = () =>
finish(() => reject(new Error("Paperclip tool call cancelled")));
const timer = setTimeout(() => {
finish(() => reject(new Error("Paperclip tool call timed out")));
controller.abort();
}, timeoutMs);
timer.unref();
controller.signal.addEventListener("abort", onAbort, { once: true });
promise.then(
(value) => finish(() => resolve(value)),
(error) => finish(() => reject(error)),
);
});
}
function safeError(error: unknown): string {
return (error instanceof Error ? error.message : String(error)).slice(
0,
2_000,
);
}
function successfulToolResult(
tool: string,
callId: string,
value: unknown,
): RunnerToolCallResult {
const json = strictJson(value);
if (json !== null) return chunkedToolResult(tool, callId, "json", json);
const representation = JSON.stringify({
schema: "paperclip.semantic_tool_result.v1",
status: "completed",
tool,
callIdentitySha256: createHash("sha256").update(callId).digest("hex"),
encoding: "paperclip.tagged_graph.v1",
result: taggedGraph(value),
});
return chunkedToolResult(
tool,
callId,
"paperclip.tagged_graph.v1",
representation,
);
}
function strictJson(value: unknown): string | null {
try {
const serialized = JSON.stringify(
value,
function strictJsonValue(key, candidate) {
const source = this[key];
if (
typeof candidate === "undefined" ||
typeof candidate === "bigint" ||
typeof candidate === "function" ||
typeof candidate === "symbol" ||
(typeof candidate === "number" && !Number.isFinite(candidate)) ||
(source !== null &&
typeof source === "object" &&
Reflect.ownKeys(source).some(
(property) => typeof property === "symbol",
)) ||
(candidate !== null &&
typeof candidate === "object" &&
!Array.isArray(candidate) &&
Object.getPrototypeOf(candidate) !== Object.prototype &&
Object.getPrototypeOf(candidate) !== null)
) {
throw new Error("Result requires tagged graph encoding");
}
return candidate;
},
);
return serialized ?? null;
} catch {
return null;
}
}
function chunkedToolResult(
tool: string,
callId: string,
encoding: "json" | "paperclip.tagged_graph.v1",
serialized: string,
): RunnerToolCallResult {
if (Buffer.byteLength(serialized) <= MAX_RESULT_CHUNK_BYTES) {
return { content: [{ type: "text", text: serialized }] };
}
const chunks = utf8Chunks(serialized, MAX_RESULT_CHUNK_BYTES);
const manifest = JSON.stringify({
schema: "paperclip.semantic_tool_result_chunks.v1",
status: "completed",
tool,
callIdentitySha256: createHash("sha256").update(callId).digest("hex"),
encoding,
contentOffset: 1,
chunkCount: chunks.length,
byteLength: Buffer.byteLength(serialized),
sha256: createHash("sha256").update(serialized).digest("hex"),
});
return {
content: [
{ type: "text", text: manifest },
...chunks.map((text) => ({ type: "text" as const, text })),
],
};
}
function utf8Chunks(value: string, maxBytes: number): string[] {
const bytes = Buffer.from(value);
const chunks: string[] = [];
let start = 0;
while (start < bytes.length) {
let end = Math.min(start + maxBytes, bytes.length);
while (end < bytes.length && (bytes[end]! & 0xc0) === 0x80) end -= 1;
if (end === start) {
throw new Error("Runner tool result chunk boundary is invalid");
}
chunks.push(bytes.subarray(start, end).toString("utf8"));
start = end;
}
return chunks;
}
interface TaggedGraphState {
nodes: Array<Record<string, unknown>>;
objects: Map<object, number>;
symbols: Map<symbol, number>;
}
function taggedGraph(value: unknown): Record<string, unknown> {
const state: TaggedGraphState = {
nodes: [],
objects: new Map(),
symbols: new Map(),
};
return {
root: taggedValue(value, state),
nodes: state.nodes,
};
}
function taggedValue(value: unknown, state: TaggedGraphState): unknown {
if (
value === null ||
typeof value === "string" ||
typeof value === "boolean"
) {
return value;
}
if (typeof value === "number") {
if (Number.isFinite(value)) return value;
return {
$type: "number",
value: Number.isNaN(value)
? "NaN"
: value === Number.POSITIVE_INFINITY
? "Infinity"
: "-Infinity",
};
}
if (typeof value === "undefined") return { $type: "undefined" };
if (typeof value === "bigint") {
return { $type: "bigint", value: value.toString() };
}
if (typeof value === "symbol") return taggedSymbol(value, state);
const object = value as object;
const existing = state.objects.get(object);
if (existing !== undefined) return { $ref: existing };
const id = state.nodes.length + 1;
state.objects.set(object, id);
const node: Record<string, unknown> = {
id,
type: taggedObjectType(value),
};
state.nodes.push(node);
if (typeof value === "function") {
node.source = Function.prototype.toString.call(value);
} else if (value instanceof Date) {
node.value = Number.isNaN(value.getTime())
? "Invalid Date"
: value.toISOString();
} else if (value instanceof RegExp) {
node.source = value.source;
node.flags = value.flags;
node.lastIndex = value.lastIndex;
} else if (value instanceof Map) {
node.entries = [...value.entries()].map(([key, entry]) => [
taggedValue(key, state),
taggedValue(entry, state),
]);
} else if (value instanceof Set) {
node.entries = [...value.values()].map((entry) =>
taggedValue(entry, state),
);
} else if (value instanceof ArrayBuffer) {
node.base64 = Buffer.from(value).toString("base64");
} else if (ArrayBuffer.isView(value)) {
node.base64 = Buffer.from(
value.buffer,
value.byteOffset,
value.byteLength,
).toString("base64");
}
node.properties = Reflect.ownKeys(value).map((key) => {
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
return {
key: typeof key === "string" ? key : taggedSymbol(key, state),
enumerable: descriptor.enumerable,
configurable: descriptor.configurable,
...(Object.hasOwn(descriptor, "value")
? {
writable: descriptor.writable,
value: taggedValue(descriptor.value, state),
}
: {
get:
descriptor.get === undefined
? null
: Function.prototype.toString.call(descriptor.get),
set:
descriptor.set === undefined
? null
: Function.prototype.toString.call(descriptor.set),
}),
};
});
return { $ref: id };
}
function taggedSymbol(
value: symbol,
state: TaggedGraphState,
): Record<string, unknown> {
const existing = state.symbols.get(value);
if (existing !== undefined) return { $symbolRef: existing };
const id = state.symbols.size + 1;
state.symbols.set(value, id);
return {
$type: "symbol",
id,
key: Symbol.keyFor(value) ?? null,
description: value.description ?? null,
};
}
function taggedObjectType(value: object): string {
if (Array.isArray(value)) return "Array";
if (typeof value === "function") return "Function";
if (ArrayBuffer.isView(value)) return value.constructor.name;
const prototype = Object.getPrototypeOf(value) as {
constructor?: { name?: unknown };
} | null;
return typeof prototype?.constructor?.name === "string"
? prototype.constructor.name
: "Object";
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (isRecord(value)) {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
function isJsonContentType(value: string | undefined): boolean {
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
}
function rpcIdKey(value: string | number): string {
return `${typeof value}:${String(value)}`;
}
function positiveBoundedInteger(
value: number | undefined,
fallback: number,
minimum: number,
maximum: number,
label: string,
): number {
const resolved = value ?? fallback;
if (
!Number.isSafeInteger(resolved) ||
resolved < minimum ||
resolved > maximum
) {
throw new Error(
`Runner tool bridge ${label} is outside its supported bound`,
);
}
return resolved;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}