// Fake `codex app-server` fixture for appserver-connector.test.ts, spawned as// a child process with `bun run <this file>`. Speaks the app-server JSON-RPC// protocol over newline-delimited stdio with the same shapes the real binary// uses (verified against codex-rs/app-server-protocol v2), including://// - handshake ordering: `thread/start` is refused until the `initialized`// notification has arrived, so the bridge's sequence is asserted here;// - a paginated `mcpServerStatus/list` whose FIRST page holds a different// server, so the bridge must follow `nextCursor`;// - a `needs_approval` tool that emits a server→client// `mcpServer/elicitation/request` and only succeeds when the answer is// an accept — the round trip through executor's elicitation bridge.import * as readline from "node:readline"; import { Option, Schema } from "effect"; const decodeMessage = Schema.decodeUnknownOption( Schema.fromJsonString( Schema.Struct({ id: Schema.optional(Schema.Union([Schema.Number, Schema.String])), method: Schema.optional(Schema.String), params: Schema.optional(Schema.Unknown), result: Schema.optional(Schema.Unknown), }), ),); const decodeInitializeParams = Schema.decodeUnknownOption( Schema.Struct({ clientInfo: Schema.Struct({ name: Schema.String }) }),); const decodeThreadStartParams = Schema.decodeUnknownOption( Schema.Struct({ approvalPolicy: Schema.optional(Schema.String) }),); const decodeThreadParams = Schema.decodeUnknownOption( Schema.Struct({ threadId: Schema.optional(Schema.String), cursor: Schema.optional(Schema.String), }),); const decodeToolCallParams = Schema.decodeUnknownOption( Schema.Struct({ threadId: Schema.String, server: Schema.String, tool: Schema.String, arguments: Schema.optional(Schema.Unknown), _meta: Schema.optional(Schema.Unknown), }),); const decodeElicitAnswer = Schema.decodeUnknownOption( Schema.Struct({ action: Schema.String, content: Schema.optional(Schema.Unknown) }),); const THREAD_ID = "thread-fixture-1"; const TOOLS = { echo: { name: "echo", description: "Echo the arguments back", inputSchema: { type: "object", properties: { text: { type: "string" } } }, }, announce_restart: { name: "announce_restart", description: "Emit server status notifications", inputSchema: { type: "object", properties: {} }, }, needs_approval: { name: "needs_approval", description: "Succeeds only after an accepted elicitation", inputSchema: { type: "object", properties: {} }, },}; const write = (message: object): void => { process.stdout.write(`${JSON.stringify(message)}\n`);}; const reply = (id: number | string, result: unknown): void => { write({ jsonrpc: "2.0", id, result });}; const replyError = (id: number | string, code: number, message: string): void => { write({ jsonrpc: "2.0", id, error: { code, message } });}; let initializedSeen = false;let elicitationsAllowed = false;let nextServerRequestId = 1000;/** Elicitation request id → the pending tool call's request id. */const pendingApprovals = new Map<number | string, number | string>(); /** The `node_repl` server, as Codex exposes it: one raw `js` REPL tool. The * sky surface is projected onto this by the bridge, so the fixture only has * to echo back the program it was asked to run. */const NODE_REPL_TOOLS = { js: { name: "js", description: "JavaScript code to execute with top-level await.", inputSchema: { type: "object", properties: { code: { type: "string" } } }, },}; const serverStatusPage = (cursor: string | undefined): object => cursor === undefined ? { data: [ { name: "decoy", runtimeStatus: "connected", pluginId: null, serverInfo: null, tools: {}, resources: [], resourceTemplates: [], authStatus: "unsupported", }, ], nextCursor: "page-2", } : { data: [ { name: "messages", runtimeStatus: "connected", pluginId: "messages", serverInfo: null, tools: TOOLS, resources: [], resourceTemplates: [], authStatus: "unsupported", }, { name: "node_repl", runtimeStatus: "connected", pluginId: null, serverInfo: null, tools: NODE_REPL_TOOLS, resources: [], resourceTemplates: [], authStatus: "unsupported", }, ], nextCursor: null, }; const handleToolCall = (id: number | string, params: unknown): void => { const decoded = decodeToolCallParams(params); if (Option.isNone(decoded)) { replyError(id, -32602, "malformed mcpServer/tool/call params"); return; } const call = decoded.value; // `node_repl` echoes the program it was handed, so a test can assert what // the sky surface compiled without needing a real REPL. if (call.server === "node_repl") { const args = call.arguments as { code?: string } | undefined; // A Chrome-shaped per-site approval: no schema to fill in, and the terms // of the grant (`persist`, `origin`) carried in `_meta`. if (args?.code?.includes("__needs_site_approval")) { const elicitationId = nextServerRequestId++; pendingApprovals.set(elicitationId, id); write({ jsonrpc: "2.0", id: elicitationId, method: "mcpServer/elicitation/request", params: { threadId: THREAD_ID, turnId: null, serverName: "node_repl", mode: "form", message: "Allow Browser use to access https://example.com?", requestedSchema: { type: "object", properties: {} }, _meta: { connector_id: "browser-use", connector_name: "Browser use", origin: "https://example.com", persist: "always", }, }, }); return; } reply(id, { content: [{ type: "text", text: args?.code ?? "" }], // Echoed so a test can assert the turn metadata the Chrome client // requires, without needing a real browser. structuredContent: { meta: call._meta ?? null }, }); return; } if (call.threadId !== THREAD_ID || call.server !== "messages") { replyError(id, -32602, `unknown thread or server: ${call.threadId}/${call.server}`); return; } if (call.tool === "announce_restart") { // Codex reports a server's startup transitions; it names every server it // runs, so the fixture emits a decoy alongside the real one. write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "someone-else", status: "ready", error: null }, }); write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "messages", status: "starting", error: null }, }); write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "messages", status: "ready", error: null }, }); reply(id, { content: [{ type: "text", text: "announced" }] }); return; } if (call.tool === "echo") { reply(id, { content: [{ type: "text", text: JSON.stringify(call.arguments ?? {}) }], structuredContent: { codexHome: process.env["CODEX_HOME"] ?? null }, isError: null, }); return; } if (call.tool === "needs_approval") { if (!elicitationsAllowed) { // Exactly what Codex returns when it declines the elicitation for the // client: an error result, no prompt, no explanation. reply(id, { content: [{ type: "text", text: "access was not approved" }], isError: true, }); return; } const elicitationId = nextServerRequestId++; pendingApprovals.set(elicitationId, id); write({ jsonrpc: "2.0", id: elicitationId, method: "mcpServer/elicitation/request", params: { threadId: THREAD_ID, turnId: null, serverName: "messages", mode: "form", _meta: null, message: "Allow the fixture to proceed?", requestedSchema: { type: "object", properties: {} }, }, }); return; } replyError(id, -32602, `unknown tool: ${call.tool}`);}; const handleElicitationAnswer = (id: number | string, result: unknown): void => { const callId = pendingApprovals.get(id); if (callId === undefined) return; pendingApprovals.delete(id); const answer = Option.getOrUndefined(decodeElicitAnswer(result)); if (answer?.action === "accept") { reply(callId, { content: [{ type: "text", text: "approved" }] }); return; } reply(callId, { content: [{ type: "text", text: `denied: ${answer?.action ?? "unreadable"}` }], isError: true, });}; readline.createInterface({ input: process.stdin }).on("line", (line) => { const decoded = decodeMessage(line); if (Option.isNone(decoded)) return; const message = decoded.value; if (message.method === undefined) { // A response from the bridge — only elicitation answers flow this way. if (message.id !== undefined) handleElicitationAnswer(message.id, message.result); return; } if (message.method === "initialized") { initializedSeen = true; return; } if (message.id === undefined) return; if (message.method === "initialize") { const params = decodeInitializeParams(message.params); if (Option.isNone(params)) { replyError(message.id, -32602, "initialize requires clientInfo"); return; } reply(message.id, { userAgent: "codex-fixture/0.0.0" }); return; } if (message.method === "thread/start") { if (!initializedSeen) { replyError(message.id, -32600, "thread/start before the initialized notification"); return; } // Codex DECLINES every MCP elicitation itself on a thread whose approval // policy does not allow them, so a thread started without one can never // receive an approval prompt. The fixture models that: it only elicits // when the bridge asked for a policy that permits elicitations. const params = Option.getOrUndefined(decodeThreadStartParams(message.params)); elicitationsAllowed = params?.approvalPolicy === "on-request"; reply(message.id, { thread: { id: THREAD_ID } }); return; } if (message.method === "mcpServerStatus/list") { const params = Option.getOrUndefined(decodeThreadParams(message.params)); if (params?.threadId !== THREAD_ID) { replyError(message.id, -32602, "mcpServerStatus/list requires the started threadId"); return; } reply(message.id, serverStatusPage(params.cursor)); return; } if (message.method === "mcpServer/tool/call") { handleToolCall(message.id, message.params); return; } replyError(message.id, -32601, `fixture does not implement ${message.method}`);});
// Fake `codex app-server` fixture for appserver-connector.test.ts, spawned as
// a child process with `bun run <this file>`. Speaks the app-server JSON-RPC
// protocol over newline-delimited stdio with the same shapes the real binary
// uses (verified against codex-rs/app-server-protocol v2), including:
//
// - handshake ordering: `thread/start` is refused until the `initialized`
// notification has arrived, so the bridge's sequence is asserted here;
// - a paginated `mcpServerStatus/list` whose FIRST page holds a different
// server, so the bridge must follow `nextCursor`;
// - a `needs_approval` tool that emits a server→client
// `mcpServer/elicitation/request` and only succeeds when the answer is
// an accept — the round trip through executor's elicitation bridge.
import * as readline from "node:readline";
import { Option, Schema } from "effect";
const decodeMessage = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({
id: Schema.optional(Schema.Union([Schema.Number, Schema.String])),
method: Schema.optional(Schema.String),
params: Schema.optional(Schema.Unknown),
result: Schema.optional(Schema.Unknown),
}),
),
);
const decodeInitializeParams = Schema.decodeUnknownOption(
Schema.Struct({ clientInfo: Schema.Struct({ name: Schema.String }) }),
);
const decodeThreadStartParams = Schema.decodeUnknownOption(
Schema.Struct({ approvalPolicy: Schema.optional(Schema.String) }),
);
const decodeThreadParams = Schema.decodeUnknownOption(
Schema.Struct({
threadId: Schema.optional(Schema.String),
cursor: Schema.optional(Schema.String),
}),
);
const decodeToolCallParams = Schema.decodeUnknownOption(
Schema.Struct({
threadId: Schema.String,
server: Schema.String,
tool: Schema.String,
arguments: Schema.optional(Schema.Unknown),
_meta: Schema.optional(Schema.Unknown),
}),
);
const decodeElicitAnswer = Schema.decodeUnknownOption(
Schema.Struct({ action: Schema.String, content: Schema.optional(Schema.Unknown) }),
);
const THREAD_ID = "thread-fixture-1";
const TOOLS = {
echo: {
name: "echo",
description: "Echo the arguments back",
inputSchema: { type: "object", properties: { text: { type: "string" } } },
},
announce_restart: {
name: "announce_restart",
description: "Emit server status notifications",
inputSchema: { type: "object", properties: {} },
},
needs_approval: {
name: "needs_approval",
description: "Succeeds only after an accepted elicitation",
inputSchema: { type: "object", properties: {} },
},
};
const write = (message: object): void => {
process.stdout.write(`${JSON.stringify(message)}\n`);
};
const reply = (id: number | string, result: unknown): void => {
write({ jsonrpc: "2.0", id, result });
};
const replyError = (id: number | string, code: number, message: string): void => {
write({ jsonrpc: "2.0", id, error: { code, message } });
};
let initializedSeen = false;
let elicitationsAllowed = false;
let nextServerRequestId = 1000;
/** Elicitation request id → the pending tool call's request id. */
const pendingApprovals = new Map<number | string, number | string>();
/** The `node_repl` server, as Codex exposes it: one raw `js` REPL tool. The
* sky surface is projected onto this by the bridge, so the fixture only has
* to echo back the program it was asked to run. */
const NODE_REPL_TOOLS = {
js: {
name: "js",
description: "JavaScript code to execute with top-level await.",
inputSchema: { type: "object", properties: { code: { type: "string" } } },
},
};
const serverStatusPage = (cursor: string | undefined): object =>
cursor === undefined
? {
data: [
{
name: "decoy",
runtimeStatus: "connected",
pluginId: null,
serverInfo: null,
tools: {},
resources: [],
resourceTemplates: [],
authStatus: "unsupported",
},
],
nextCursor: "page-2",
}
: {
data: [
{
name: "messages",
runtimeStatus: "connected",
pluginId: "messages",
serverInfo: null,
tools: TOOLS,
resources: [],
resourceTemplates: [],
authStatus: "unsupported",
},
{
name: "node_repl",
runtimeStatus: "connected",
pluginId: null,
serverInfo: null,
tools: NODE_REPL_TOOLS,
resources: [],
resourceTemplates: [],
authStatus: "unsupported",
},
],
nextCursor: null,
};
const handleToolCall = (id: number | string, params: unknown): void => {
const decoded = decodeToolCallParams(params);
if (Option.isNone(decoded)) {
replyError(id, -32602, "malformed mcpServer/tool/call params");
return;
}
const call = decoded.value;
// `node_repl` echoes the program it was handed, so a test can assert what
// the sky surface compiled without needing a real REPL.
if (call.server === "node_repl") {
const args = call.arguments as { code?: string } | undefined;
// A Chrome-shaped per-site approval: no schema to fill in, and the terms
// of the grant (`persist`, `origin`) carried in `_meta`.
if (args?.code?.includes("__needs_site_approval")) {
const elicitationId = nextServerRequestId++;
pendingApprovals.set(elicitationId, id);
write({
jsonrpc: "2.0",
id: elicitationId,
method: "mcpServer/elicitation/request",
params: {
threadId: THREAD_ID,
turnId: null,
serverName: "node_repl",
mode: "form",
message: "Allow Browser use to access https://example.com?",
requestedSchema: { type: "object", properties: {} },
_meta: {
connector_id: "browser-use",
connector_name: "Browser use",
origin: "https://example.com",
persist: "always",
},
},
});
return;
}
reply(id, {
content: [{ type: "text", text: args?.code ?? "" }],
// Echoed so a test can assert the turn metadata the Chrome client
// requires, without needing a real browser.
structuredContent: { meta: call._meta ?? null },
});
return;
}
if (call.threadId !== THREAD_ID || call.server !== "messages") {
replyError(id, -32602, `unknown thread or server: ${call.threadId}/${call.server}`);
return;
}
if (call.tool === "announce_restart") {
// Codex reports a server's startup transitions; it names every server it
// runs, so the fixture emits a decoy alongside the real one.
write({
jsonrpc: "2.0",
method: "mcpServer/startupStatus/updated",
params: { threadId: THREAD_ID, name: "someone-else", status: "ready", error: null },
});
write({
jsonrpc: "2.0",
method: "mcpServer/startupStatus/updated",
params: { threadId: THREAD_ID, name: "messages", status: "starting", error: null },
});
write({
jsonrpc: "2.0",
method: "mcpServer/startupStatus/updated",
params: { threadId: THREAD_ID, name: "messages", status: "ready", error: null },
});
reply(id, { content: [{ type: "text", text: "announced" }] });
return;
}
if (call.tool === "echo") {
reply(id, {
content: [{ type: "text", text: JSON.stringify(call.arguments ?? {}) }],
structuredContent: { codexHome: process.env["CODEX_HOME"] ?? null },
isError: null,
});
return;
}
if (call.tool === "needs_approval") {
if (!elicitationsAllowed) {
// Exactly what Codex returns when it declines the elicitation for the
// client: an error result, no prompt, no explanation.
reply(id, {
content: [{ type: "text", text: "access was not approved" }],
isError: true,
});
return;
}
const elicitationId = nextServerRequestId++;
pendingApprovals.set(elicitationId, id);
write({
jsonrpc: "2.0",
id: elicitationId,
method: "mcpServer/elicitation/request",
params: {
threadId: THREAD_ID,
turnId: null,
serverName: "messages",
mode: "form",
_meta: null,
message: "Allow the fixture to proceed?",
requestedSchema: { type: "object", properties: {} },
},
});
return;
}
replyError(id, -32602, `unknown tool: ${call.tool}`);
};
const handleElicitationAnswer = (id: number | string, result: unknown): void => {
const callId = pendingApprovals.get(id);
if (callId === undefined) return;
pendingApprovals.delete(id);
const answer = Option.getOrUndefined(decodeElicitAnswer(result));
if (answer?.action === "accept") {
reply(callId, { content: [{ type: "text", text: "approved" }] });
return;
}
reply(callId, {
content: [{ type: "text", text: `denied: ${answer?.action ?? "unreadable"}` }],
isError: true,
});
};
readline.createInterface({ input: process.stdin }).on("line", (line) => {
const decoded = decodeMessage(line);
if (Option.isNone(decoded)) return;
const message = decoded.value;
if (message.method === undefined) {
// A response from the bridge — only elicitation answers flow this way.
if (message.id !== undefined) handleElicitationAnswer(message.id, message.result);
return;
}
if (message.method === "initialized") {
initializedSeen = true;
return;
}
if (message.id === undefined) return;
if (message.method === "initialize") {
const params = decodeInitializeParams(message.params);
if (Option.isNone(params)) {
replyError(message.id, -32602, "initialize requires clientInfo");
return;
}
reply(message.id, { userAgent: "codex-fixture/0.0.0" });
return;
}
if (message.method === "thread/start") {
if (!initializedSeen) {
replyError(message.id, -32600, "thread/start before the initialized notification");
return;
}
// Codex DECLINES every MCP elicitation itself on a thread whose approval
// policy does not allow them, so a thread started without one can never
// receive an approval prompt. The fixture models that: it only elicits
// when the bridge asked for a policy that permits elicitations.
const params = Option.getOrUndefined(decodeThreadStartParams(message.params));
elicitationsAllowed = params?.approvalPolicy === "on-request";
reply(message.id, { thread: { id: THREAD_ID } });
return;
}
if (message.method === "mcpServerStatus/list") {
const params = Option.getOrUndefined(decodeThreadParams(message.params));
if (params?.threadId !== THREAD_ID) {
replyError(message.id, -32602, "mcpServerStatus/list requires the started threadId");
return;
}
reply(message.id, serverStatusPage(params.cursor));
return;
}
if (message.method === "mcpServer/tool/call") {
handleToolCall(message.id, message.params);
return;
}
replyError(message.id, -32601, `fixture does not implement ${message.method}`);
});