Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/choosekit-mcp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/choosekit-mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "choosekit-mcp",
"version": "0.6.0",
"version": "0.6.1",
"description": "Use choosekit as an MCP server.",
"license": "Apache-2.0",
"author": {
Expand Down Expand Up @@ -37,7 +37,7 @@
},
"dependencies": {
"@modelcontextprotocol/server": "2.0.0",
"choosekit": "^0.9.0",
"choosekit": "^0.9.1",
"zod": "^4.6.4"
},
"devDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion packages/choosekit-mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ function errorResult(error: unknown, signal: AbortSignal) {
throw error;
}
if (error instanceof ScoringError) {
const status = error.httpStatus;
const text = typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
? `The scoring backend returned HTTP ${status}.`
: "The model could not score the supplied choices.";
return {
isError: true as const,
content: [{ type: "text" as const, text: "The model could not score the supplied choices." }],
content: [{ type: "text" as const, text }],
};
}
if (error instanceof ImageLoadError) {
Expand Down
30 changes: 30 additions & 0 deletions packages/choosekit-mcp/tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,33 @@ test("serves a minimal-prefix choice using the configured llama.cpp endpoint", a
assert.equal(many.result.structuredContent.followup.choice, "billing");
for (const line of process_.lines) assert.doesNotThrow(() => JSON.parse(line));
});

test("CLI preserves adapter HTTP 429 in both tool errors", async (t) => {
const preload = `globalThis.fetch = async () => Response.json(
{ error: "secret upstream details" }, { status: 429 });`;
const fixture = `data:text/javascript;base64,${Buffer.from(preload).toString("base64")}`;
const process_ = startCli(cliEnvironment({
CHOOSEKIT_BACKEND: "openrouter",
CHOOSEKIT_BASE_URL: "",
CHOOSEKIT_MODEL: "fixture/model",
OPENROUTER_API_KEY: "secret-api-key",
}), ["--import", fixture]);
t.after(() => process_.close());
await initialize(process_);

for (const [name, arguments_] of [
["choose", request],
["chooseMany", {
context: request.context,
questions: { route: { question: request.question, choices: request.choices } },
}],
]) {
const response = await process_.rpc("tools/call", { name, arguments: arguments_ });
assert.equal(response.error, undefined);
assert.equal(response.result.isError, true);
assert.deepEqual(response.result.content,
[{ type: "text", text: "The scoring backend returned HTTP 429." }]);
assert.equal(response.result.structuredContent, undefined);
assert.doesNotMatch(JSON.stringify(response), /secret/);
}
});
43 changes: 43 additions & 0 deletions packages/choosekit-mcp/tests/server.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,46 @@ test("preserves constructor as an ordinary choice key", async (t) => {
assert.equal(response.result.structuredContent.choice, "constructor");
assert.deepEqual(receivedChoices, arguments_.choices);
});

for (const [tool, arguments_] of [["choose", request], ["chooseMany", manyRequest]]) {
test(`${tool} reports only validated HTTP metadata and recovers after failure`, async (t) => {
const choose = createChooser(() => ({ logprobs: [-0.1, -2], boundaryTokens: 1 }));
let failure;
const connection = await connect(async (input) => {
if (failure) throw failure;
return choose(input);
});
t.after(() => connection.close());

const cases = [
...[100, 200, 302, 401, 403, 429, 503, 599].map((httpStatus) => [
new ScoringError("secret upstream details", {
httpStatus, cause: new Error("secret cause"),
}),
`The scoring backend returned HTTP ${httpStatus}.`,
]),
...[undefined, null, "401", 99, 600, 401.5, NaN, Infinity, -Infinity,
{ toString() { throw new Error("must not coerce"); } }].map((httpStatus) => [
new ScoringError("secret upstream details", { httpStatus }),
"The model could not score the supplied choices.",
]),
[Object.assign(new Error("secret implementation detail"), { httpStatus: 401 }),
"The choice request failed unexpectedly."],
];

for (const [error, text] of cases) {
failure = error;
const response = await connection.rpc("tools/call", { name: tool, arguments: arguments_ });
assert.equal(response.error, undefined);
assert.equal(response.result.isError, true);
assert.deepEqual(response.result.content, [{ type: "text", text }]);
assert.equal(response.result.structuredContent, undefined);
assert.doesNotMatch(JSON.stringify(response), /secret/);
}

failure = undefined;
const response = await connection.rpc("tools/call", { name: tool, arguments: arguments_ });
assert.equal(response.error, undefined);
assert.notEqual(response.result.isError, true);
});
}
Loading