Harden MCP schema and wire Claude OAuth/token handling

This commit is contained in:
2026-02-23 14:48:01 -05:00
parent ef2a25b5fb
commit 62e2491cde
14 changed files with 770 additions and 56 deletions

View File

@@ -0,0 +1,217 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { loadConfig } from "../src/config.js";
import {
parseCodexFinalResponse,
runCodexPrompt,
} from "../src/examples/codex.js";
import {
readClaudeResult,
runClaudePrompt,
} from "../src/examples/claude.js";
import type { SessionContext } from "../src/examples/session-context.js";
type ClaudeQueryFunction = (typeof import("@anthropic-ai/claude-agent-sdk"))["query"];
function createMessageStream(
messages: SDKMessage[],
): AsyncIterable<SDKMessage> {
return {
async *[Symbol.asyncIterator]() {
for (const message of messages) {
yield message;
}
},
};
}
test("runCodexPrompt wires client options and parses final output", async () => {
const config = loadConfig({
CODEX_API_KEY: "codex-token",
OPENAI_BASE_URL: "https://api.example.com/v1",
});
let capturedClientInput: Record<string, unknown> | undefined;
let capturedThreadInput:
| {
workingDirectory: string;
skipGitRepoCheck: boolean;
}
| undefined;
let capturedPrompt = "";
let closed = false;
let output = "";
const sessionContext: SessionContext = {
provider: "codex",
sessionId: "session-codex",
mcp: {
codexConfig: {
mcp_servers: {
local: {
command: "node",
args: ["server.js"],
},
},
},
},
promptWithContext: "prompt with context",
runtimeInjection: {
workingDirectory: "/tmp/worktree",
env: {
PATH: "/usr/bin",
},
discoveryFilePath: "/tmp/worktree/.agent-context/resources.json",
},
runInSession: async <T>(run: () => Promise<T>) => run(),
close: async () => {
closed = true;
},
};
await runCodexPrompt("ignored", {
config,
createSessionContextFn: async () => sessionContext,
createCodexClient: (input) => {
capturedClientInput = input as Record<string, unknown>;
return {
startThread: (threadInput) => {
capturedThreadInput = threadInput;
return {
run: async (prompt) => {
capturedPrompt = prompt;
return {
finalResponse: " completed ",
};
},
};
},
};
},
writeOutput: (line) => {
output = line;
},
});
assert.equal(capturedClientInput?.["apiKey"], "codex-token");
assert.equal(capturedClientInput?.["baseUrl"], "https://api.example.com/v1");
assert.deepEqual(capturedClientInput?.["config"], sessionContext.mcp.codexConfig);
assert.deepEqual(capturedClientInput?.["env"], sessionContext.runtimeInjection.env);
assert.deepEqual(capturedThreadInput, {
workingDirectory: "/tmp/worktree",
skipGitRepoCheck: true,
});
assert.equal(capturedPrompt, "prompt with context");
assert.equal(output, "completed");
assert.equal(closed, true);
});
test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
const config = loadConfig({
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
ANTHROPIC_API_KEY: "legacy-api-key",
CLAUDE_MODEL: "claude-sonnet-4-6",
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
});
let closed = false;
let output = "";
let queryInput:
| {
prompt: string;
options?: Record<string, unknown>;
}
| undefined;
const sessionContext: SessionContext = {
provider: "claude",
sessionId: "session-claude",
mcp: {
claudeMcpServers: {
local: {
type: "stdio",
command: "node",
args: ["server.js"],
},
},
},
promptWithContext: "augmented prompt",
runtimeInjection: {
workingDirectory: "/tmp/claude-worktree",
env: {
PATH: "/usr/bin",
ANTHROPIC_API_KEY: "ambient-api-key",
},
discoveryFilePath: "/tmp/claude-worktree/.agent-context/resources.json",
},
runInSession: async <T>(run: () => Promise<T>) => run(),
close: async () => {
closed = true;
},
};
const queryFn: ClaudeQueryFunction = ((input: {
prompt: string;
options?: Record<string, unknown>;
}) => {
queryInput = input;
const stream = createMessageStream([
{
type: "result",
subtype: "success",
result: " response text ",
} as SDKMessage,
]);
return {
...stream,
close: () => {},
} as ReturnType<ClaudeQueryFunction>;
}) as ClaudeQueryFunction;
await runClaudePrompt("ignored", {
config,
createSessionContextFn: async () => sessionContext,
queryFn,
writeOutput: (line) => {
output = line;
},
});
assert.equal(queryInput?.prompt, "augmented prompt");
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
assert.equal(queryInput?.options?.authToken, "oauth-token");
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
const env = queryInput?.options?.env as Record<string, string | undefined> | undefined;
assert.equal(env?.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token");
assert.equal(env?.ANTHROPIC_API_KEY, undefined);
assert.equal(output, "response text");
assert.equal(closed, true);
});
test("readClaudeResult throws on non-success result events", async () => {
const stream = createMessageStream([
{
type: "result",
subtype: "error_during_execution",
errors: ["first", "second"],
} as SDKMessage,
]);
await assert.rejects(
() => readClaudeResult(stream),
/Claude query failed \(error_during_execution\): first; second/,
);
});
test("parseCodexFinalResponse keeps fallback text for empty responses", () => {
const response = parseCodexFinalResponse({
finalResponse: " ",
});
assert.equal(response, "(No response text returned)");
});