fix: resolve CLI mcp call protocol issues (BUG-001) (#10960)

Validated on the combined 8-PR board: all seven CLI command suites green (combo-suggest, compression, mcp-call, oneproxy, resilience, skills + the shared mcpStreamMock helper) within the board's 88/88, typecheck:core clean, gates within baseline. The CLI mcp call protocol fixes (BUG-001) land with full regression coverage across the affected commands. Thank you @YunyunZhai — and thank you for your patience while this one waited for review.
This commit is contained in:
Jack Smith
2026-08-24 05:11:50 +08:00
committed by GitHub
parent 3abbb60ec6
commit 14d70a755b
14 changed files with 812 additions and 526 deletions

View File

@@ -3,6 +3,7 @@ import { printHeading } from "../io.mjs";
import { withRuntime } from "../runtime.mjs";
import { t } from "../i18n.mjs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { resolveComboModels, collectModel } from "./comboModels.mjs";
@@ -63,15 +64,7 @@ export function extendComboSuggest(combo) {
weights: opts.weights ? JSON.parse(opts.weights) : undefined,
top: opts.top,
};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_best_combo_for_task", arguments: body },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const data = await mcpCallTool("omniroute_best_combo_for_task", body);
const candidates = data.candidates ?? data;
const rows = (Array.isArray(candidates) ? candidates : []).map((c, i) => ({
rank: i + 1,

View File

@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -78,18 +79,17 @@ async function restComboStats(period) {
}
async function mcpCall(name, args, restFallback) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (res.ok) return res.json();
// 404 = MCP tool surface not mounted on this build; 501 = not implemented.
// Anything else is a genuine error and we surface it.
if ((res.status === 404 || res.status === 501) && typeof restFallback === "function") {
return restFallback();
try {
return await mcpCallTool(name, args);
} catch (err) {
// Keep the REST fallback behavior for builds where the MCP surface
// is unreachable / not mounted. Anything else rethrows as an error.
const status = err?.status || err?.cause?.status;
if ((status === 404 || status === 501) && typeof restFallback === "function") {
return restFallback();
}
throw err;
}
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
async function confirm(q) {

View File

@@ -61,27 +61,12 @@ export function registerMcp(program) {
? JSON.parse(argsPositional)
: {};
if (opts.stream) {
await runMcpStream(tool, args, globalOpts);
return;
}
const exitCode = await runMcpCallCommand(tool, args, {
...opts,
stream: opts.stream,
}, globalOpts);
const extraHeaders = opts.scope?.length ? { "X-MCP-Scopes": opts.scope.join(",") } : {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: tool, arguments: args },
headers: extraHeaders,
});
if (res.status === 403) {
process.stderr.write("Scope denied\n");
process.exit(4);
}
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
if (exitCode !== 0) process.exit(exitCode);
});
mcp
@@ -99,112 +84,132 @@ export function registerMcp(program) {
const data = await res.json();
emit(data.scopes ?? data, cmd.optsWithGlobals());
});
// 5.2 — mcp tools + mcp audit
const tools = mcp.command("tools").description(t("mcp.tools.description"));
tools
.command("list")
.description(t("mcp.tools.list.description"))
.option("--scope <s>", t("mcp.tools.list.scope"))
.action(async (opts, cmd) => {
const params = new URLSearchParams();
if (opts.scope) params.set("scope", opts.scope);
const res = await apiFetch(`/api/mcp/tools?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.tools ?? data, cmd.optsWithGlobals(), mcpToolSchema);
});
tools
.command("info <name>")
.description(t("mcp.tools.info.description"))
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tools
.command("schema <name>")
.description(t("mcp.tools.schema.description"))
.option("--io <kind>", t("mcp.tools.schema.io"), "input")
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}&io=${opts.io}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
const data = await res.json();
const globalOpts = cmd.optsWithGlobals();
if (globalOpts.output === "json") {
process.stdout.write(JSON.stringify(data.schema ?? data, null, 2) + "\n");
} else {
emit(data.schema ?? data, globalOpts);
}
});
const audit = mcp.command("audit").description(t("mcp.audit.description"));
audit
.command("tail")
.option("--follow", t("audit.tail.follow"))
.option("--limit <n>", t("audit.tail.limit"), parseInt, 100)
.action(async (opts, cmd) => {
const { runAuditTail } = await import("./audit.mjs");
await runAuditTail({ ...opts, source: "mcp" }, cmd);
});
audit
.command("stats")
.option("--period <p>", t("audit.stats.period"), "7d")
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/mcp/audit/stats?period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
async function runMcpStream(tool, args, globalOpts) {
/**
* Shared JSON-RPC 2.0 MCP client used by both stream and non-stream `mcp call`.
*
* Protocol:
* 1. POST /api/mcp/stream with initialize → get Mcp-Session-Id header
* 2. POST /api/mcp/stream with tools/call + Mcp-Session-Id header
*
* When `stream` is true, writes SSE data chunks to stdout as they arrive.
* When `stream` is false, returns the parsed JSON-RPC result.
*
* Returns the exit code (0 = success, non-zero = failure).
*/
async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } = {}) {
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
const apiKey = globalOpts.apiKey ?? "";
const res = await fetch(`${baseUrl}/api/mcp/stream`, {
const streamUrl = `${baseUrl}/api/mcp/stream`;
const hdrs = {
"Content-Type": "application/json",
Accept: stream ? "text/event-stream" : "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
};
// Step 1 — initialize
const initRes = await fetch(streamUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
body: JSON.stringify({ name: tool, arguments: args }),
headers: hdrs,
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "omniroute-cli", version: "1.0" },
},
}),
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
if (!initRes.ok) {
const text = await initRes.text().catch(() => "");
process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? `${text}` : ""}\n`);
return 1;
}
const reader = res.body.getReader();
const sessionId = initRes.headers.get("mcp-session-id");
if (!sessionId) {
process.stderr.write("MCP initialize failed: no Mcp-Session-Id in response\n");
return 1;
}
// Step 2 — tools/call
const callHeaders = {
...hdrs,
"mcp-session-id": sessionId,
};
const callRes = await fetch(streamUrl, {
method: "POST",
headers: callHeaders,
body: JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name: tool, arguments: args },
}),
});
if (!callRes.ok) {
const text = await callRes.text().catch(() => "");
process.stderr.write(`MCP call failed: HTTP ${callRes.status}${text ? `${text}` : ""}\n`);
return 1;
}
if (stream) {
return readMcpSseStream(callRes.body);
}
// Non-stream: parse JSON-RPC response
const data = await callRes.json();
if (data.error) {
process.stderr.write(`MCP error: ${data.error.message || JSON.stringify(data.error)}\n`);
return 1;
}
// Print the result content
const content = data.result?.content;
if (content) {
for (const item of content) {
if (item.type === "text") {
process.stdout.write(item.text + "\n");
} else if (item.type === "resource") {
process.stdout.write(JSON.stringify(item.resource) + "\n");
} else {
process.stdout.write(JSON.stringify(item) + "\n");
}
}
} else {
process.stdout.write(JSON.stringify(data.result, null, 2) + "\n");
}
return 0;
}
async function readMcpSseStream(body) {
if (!body) return 1;
const reader = body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
const lines = buf.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
return 0;
}
export async function runMcpCallCommand(tool, args, opts = {}, globalOpts = {}) {
return mcpJsonRpcCall(tool, args, { stream: opts.stream, globalOpts });
}
export async function runMcpStatusCommand(opts = {}) {
@@ -233,7 +238,8 @@ export async function runMcpStatusCommand(opts = {}) {
}
const transport = status.transport || "stdio";
console.log(status.running ? t("mcp.running", { transport }) : t("mcp.stopped"));
const online = status.online ?? status.running;
console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped"));
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
if (status.scopes?.length) {
console.log(" Scopes:");

View File

@@ -1,4 +1,5 @@
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -8,15 +9,7 @@ function fmtTs(v) {
}
async function mcpCall(name, args) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (!res.ok) {
process.stderr.write(`MCP error: ${res.status}\n`);
process.exit(1);
}
return res.json();
return mcpCallTool(name, args);
}
const proxySchema = [

View File

@@ -1,6 +1,7 @@
import { createInterface } from "node:readline";
import { Argument } from "commander";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -166,14 +167,7 @@ export function registerResilience(program) {
])
)
.action(async (name, opts, cmd) => {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_set_resilience_profile", arguments: { profile: name } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_set_resilience_profile", { profile: name });
process.stdout.write(`Profile: ${name}\n`);
});

View File

@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -106,14 +107,7 @@ export async function runSkillsInstall(opts, cmd) {
}
export async function runSkillsEnable(id, opts, cmd) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: true } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_skills_enable", { skillId: id, enabled: true });
process.stdout.write(`Enabled: ${id}\n`);
}
@@ -122,14 +116,7 @@ export async function runSkillsDisable(id, opts, cmd) {
const ok = await confirm(`Disable ${id}?`);
if (!ok) return;
}
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: false } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_skills_enable", { skillId: id, enabled: false });
process.stdout.write(`Disabled: ${id}\n`);
}
@@ -153,16 +140,11 @@ export async function runSkillsExecute(id, opts, cmd) {
: opts.inputFile
? JSON.parse(readFileSync(opts.inputFile, "utf8"))
: {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_execute", arguments: { skillId: id, input } },
timeout: opts.timeout ?? 30000,
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const data = await mcpCallTool(
"omniroute_skills_execute",
{ skillId: id, input },
{ timeout: opts.timeout ?? 30000 },
);
emit(data, globalOpts);
}

127
bin/cli/mcpClient.mjs Normal file
View File

@@ -0,0 +1,127 @@
/**
* Shared MCP JSON-RPC client for CLI commands.
*
* The server exposes MCP through /api/mcp/stream (Streamable HTTP transport).
* Calling a tool requires:
* 1. POST initialize → get Mcp-Session-Id response header
* 2. POST tools/call with that session header
*
* Older CLI paths POSTed { name, arguments } to /api/mcp/tools/call, which is
* not a registered route, so every MCP-backed command was broken.
*
* These functions route through apiFetch so CLI auth, remote contexts and
* timeouts are handled the same way as every other management API call.
*/
import { apiFetch } from "./api.mjs";
function mcpError(message, status) {
const err = new Error(message);
if (status) err.status = status;
return err;
}
async function callMcpEndpoint(payload, { timeout, stream }) {
const res = await apiFetch("/api/mcp/stream", {
method: "POST",
body: payload,
timeout,
acceptNotOk: true,
headers: stream ? { Accept: "text/event-stream" } : {},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw mcpError(
`${payload.method} ${payload.id}: HTTP ${res.status}${text ? `${text}` : ""}`,
res.status,
);
}
return res;
}
/**
* Call an MCP tool over /api/mcp/stream.
*
* Non-stream: returns the JSON-RPC result payload.
* Stream: writes SSE `data:` chunks to stdout and returns null on success.
*/
export async function mcpCallTool(name, args = {}, options = {}) {
const { timeout, scope } = options;
const scopeHeader = scope?.length ? { "X-MCP-Scopes": scope.join(",") } : {};
const initRes = await callMcpEndpoint(
{
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "omniroute-cli", version: "1.0" },
},
},
{ timeout, stream: options.stream },
);
const sessionId = initRes.headers.get("mcp-session-id");
if (!sessionId) {
throw mcpError("MCP initialize failed: no Mcp-Session-Id in response", 500);
}
const callRes = await callMcpEndpoint(
{
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name, arguments: args },
},
{ timeout, stream: options.stream },
);
if (options.stream) {
return consumeSse(callRes.body, options.onChunk);
}
const data = await callRes.json();
if (data.error) {
const err = mcpError(`MCP error: ${data.error.message || JSON.stringify(data.error)}`);
err.code = data.error.code;
throw err;
}
if (data.result?.isError) {
const msg = data.result?.content?.[0]?.text || "unknown tool error";
throw mcpError(`MCP error: ${msg}`, 500);
}
return data.result;
}
async function consumeSse(body, onChunk) {
if (!body) throw mcpError("MCP stream returned no body", 500);
const reader = body.getReader();
const decoder = new TextDecoder();
let buf = "";
const flushLines = () => {
let idx;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx);
buf = buf.slice(idx + 1);
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") (onChunk ?? writeStdout)(raw);
}
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
flushLines();
}
buf += decoder.decode();
flushLines();
return null;
}
function writeStdout(raw) {
process.stdout.write(raw + "\n");
}

View File

@@ -1,133 +1,101 @@
import test from "node:test";
import assert from "node:assert/strict";
function makeResp(data: unknown, status = 200) {
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
};
obj.json = obj.json.bind(obj);
obj.text = obj.text.bind(obj);
return obj;
}
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
function makeCmd(output = "json") {
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
}
test("combo suggest chama omniroute_best_combo_for_task via MCP", async () => {
let capturedBody: any = null;
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
capturedUrl = url;
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(
makeResp({
candidates: [
{
name: "fast-combo",
strategy: "priority",
score: 0.92,
latencyP50Ms: 120,
costPer1k: 0.002,
},
],
rationale: "Best latency for real-time tasks",
})
);
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_best_combo_for_task",
arguments: { task: "Real-time code completions", top: 5 },
}),
globalThis.fetch = makeMcpStreamFetch({
toolResult: {
candidates: [
{
name: "fast-combo",
strategy: "priority",
score: 0.92,
latencyP50Ms: 120,
costPer1k: 0.002,
},
],
rationale: "Best latency for real-time tasks",
},
});
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
const result = await mcpCallTool("omniroute_best_combo_for_task", {
task: "Real-time code completions",
top: 5,
});
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
assert.equal(capturedBody.name, "omniroute_best_combo_for_task");
assert.equal(capturedBody.arguments.task, "Real-time code completions");
const candidates = (result as any).candidates;
assert.equal(candidates[0].name, "fast-combo");
assert.equal((result as any).rationale, "Best latency for real-time tasks");
});
test("combo suggest --max-cost/--max-latency-ms passa constraints", async () => {
let capturedBody: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ candidates: [] }));
const captured: any[] = [];
globalThis.fetch = makeMcpStreamFetch({ toolResult: { candidates: [] } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: any, init: any) => {
captured.push({ url: String(url), init });
return inner(url, init);
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_best_combo_for_task",
arguments: {
task: "Summarize PDFs",
constraints: { maxCostUsd: 0.001, maxLatencyMs: 500 },
top: 3,
},
}),
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
await mcpCallTool("omniroute_best_combo_for_task", {
task: "Summarize PDFs",
constraints: { maxCostUsd: 0.001, maxLatencyMs: 500 },
top: 3,
});
globalThis.fetch = origFetch;
assert.equal(capturedBody.arguments.constraints.maxCostUsd, 0.001);
assert.equal(capturedBody.arguments.constraints.maxLatencyMs, 500);
assert.equal(capturedBody.arguments.top, 3);
const args = JSON.parse(captured.find((c) => /tools\/call/.test(String(c.init?.body || "")))?.init?.body || "{}")?.params?.arguments;
assert.equal(args.constraints.maxCostUsd, 0.001);
assert.equal(args.constraints.maxLatencyMs, 500);
assert.equal(args.top, 3);
});
test("combo suggest --weights passa pesos no body", async () => {
let capturedBody: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ candidates: [] }));
const captured: any[] = [];
globalThis.fetch = makeMcpStreamFetch({ toolResult: { candidates: [] } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: any, init: any) => {
captured.push({ url: String(url), init });
return inner(url, init);
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_best_combo_for_task",
arguments: {
task: "batch",
weights: { latency: 0.7, cost: 0.3 },
},
}),
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
await mcpCallTool("omniroute_best_combo_for_task", {
task: "batch",
weights: { latency: 0.7, cost: 0.3 },
});
globalThis.fetch = origFetch;
assert.equal(capturedBody.arguments.weights.latency, 0.7);
assert.equal(capturedBody.arguments.weights.cost, 0.3);
const args = JSON.parse(captured.find((c) => /tools\/call/.test(String(c.init?.body || "")))?.init?.body || "{}")?.params?.arguments;
assert.equal(args.weights.latency, 0.7);
assert.equal(args.weights.cost, 0.3);
});
test("combo suggest --switch chama /api/combos/switch com melhor combo", async () => {
let urls: string[] = [];
const urls: string[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
urls.push(url);
if (url.includes("/api/mcp/tools/call")) {
return Promise.resolve(makeResp({ candidates: [{ name: "best-combo", score: 0.95 }] }));
globalThis.fetch = ((url: any, opts: any) => {
urls.push(String(url));
if (String(url).includes("/api/mcp/stream")) {
const body = opts?.body ? JSON.parse(opts.body) : {};
if (body.method === "initialize") {
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
}
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: { candidates: [{ name: "best-combo", score: 0.95 }] } }));
}
return Promise.resolve(makeResp({ switched: true }));
return Promise.resolve(makeMcpResp({ switched: true }));
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: '{"name":"omniroute_best_combo_for_task","arguments":{"task":"x"}}',
});
await (globalThis.fetch as any)("/api/combos/switch", {
method: "POST",
body: '{"name":"best-combo"}',
});
globalThis.fetch = origFetch;
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
const data = await mcpCallTool("omniroute_best_combo_for_task", { task: "x" });
const combosSwitchRes = await fetch("/api/combos/switch", { method: "POST", body: JSON.stringify({ name: (data as any).candidates[0].name }) });
assert.equal(combosSwitchRes.ok, true);
assert.ok(urls.some((u) => u.includes("/api/combos/switch")));
globalThis.fetch = origFetch;
});
test("combo.mjs exporta extendComboSuggest e registerCombo", async () => {

View File

@@ -1,11 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
function makeResp(data: unknown, status = 200) {
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
@@ -35,26 +35,32 @@ function makeCmd(output = "json") {
}
test("compression status chama omniroute_compression_status via mcp", async () => {
let capturedBody: any = null;
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: { engine: "caveman", enabled: true } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
const { runCompressionStatus } = await import("../../bin/cli/commands/compression.mjs");
await captureStdout(() => runCompressionStatus({}, makeCmd() as any));
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_compression_status");
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(body.method, "tools/call");
assert.equal(body.params.name, "omniroute_compression_status");
});
test("compression configure envia configuração via mcp", async () => {
let capturedBody: any = null;
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ success: true }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: { success: true } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
const { runCompressionConfigure } = await import("../../bin/cli/commands/compression.mjs");
@@ -63,20 +69,22 @@ test("compression configure envia configuração via mcp", async () => {
);
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_compression_configure");
// #6571: the configure command now sends the canonical `strategy` field the MCP
// tool schema (compressionConfigureInput) + handleCompressionConfigure expect,
// not the nonexistent `engine` key (which the non-strict schema silently stripped).
assert.equal(capturedBody.arguments.strategy, "caveman");
assert.ok(capturedBody.arguments.caveman?.aggressiveness === 0.8);
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(body.method, "tools/call");
assert.equal(body.params.name, "omniroute_compression_configure");
// #6571: the configure command now sends the canonical `strategy` field
assert.equal(body.params.arguments.strategy, "caveman");
assert.ok(body.params.arguments.caveman?.aggressiveness === 0.8);
});
test("compression engine set chama omniroute_set_compression_engine", async () => {
let capturedBody: any = null;
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ success: true }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
const out = await captureStdout(async () => {
@@ -85,8 +93,10 @@ test("compression engine set chama omniroute_set_compression_engine", async () =
});
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_set_compression_engine");
assert.equal(capturedBody.arguments.engine, "rtk");
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(body.method, "tools/call");
assert.equal(body.params.name, "omniroute_set_compression_engine");
assert.equal(body.params.arguments.engine, "rtk");
assert.ok(out.includes("rtk"));
});
@@ -109,6 +119,26 @@ test("compression engine set rejeita engine inválido", async () => {
assert.equal(exitCode, 2);
});
test("compression engine set normaliza hybrid → stacked alias", async () => {
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
await captureStdout(async () => {
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
await runCompressionEngineSet("hybrid", {}, makeCmd() as any);
});
globalThis.fetch = origFetch;
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(body.params.arguments.engine, "stacked");
});
test("compression rules list busca /api/compression/rules", async () => {
let capturedUrl = "";
const origFetch = globalThis.fetch;
@@ -126,10 +156,10 @@ test("compression rules list busca /api/compression/rules", async () => {
});
test("compression rules add envia pattern e action", async () => {
let capturedBody: any = null;
let capturedBody: unknown = null;
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
globalThis.fetch = ((url: string, opts: unknown) => {
capturedUrl = url;
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ id: "rule-2", pattern: ".*debug.*", action: "drop" }));
@@ -168,15 +198,19 @@ test("compression.mjs pode ser importado sem erro", async () => {
assert.equal(typeof mod.runCompressionPreview, "function");
});
// #2688 — when /api/mcp/tools/call returns 404, the CLI must fall back to
// #2688 — when the MCP tool surface returns 404, the CLI must fall back to
// direct REST endpoints (no MCP tool surface required on minimal builds).
test("compression status falls back to /api/settings/compression on MCP 404", async () => {
const callOrder: string[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
globalThis.fetch = ((url: string, opts: unknown) => {
callOrder.push(url);
if (url.includes("/api/mcp/tools/call")) {
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
if (url.includes("/api/mcp/stream")) {
const body = opts?.body ? JSON.parse(opts.body) : {};
if (body.method === "initialize") {
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
}
return Promise.resolve(makeMcpResp({ error: "not mounted" }, 404));
}
if (url.includes("/api/settings/compression")) {
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
@@ -194,48 +228,28 @@ test("compression status falls back to /api/settings/compression on MCP 404", as
await captureStdout(() => runCompressionStatus({}, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(
callOrder.some((u) => u.includes("/api/mcp/tools/call")),
"should attempt MCP first"
);
assert.ok(
callOrder.some((u) => u.includes("/api/settings/compression")),
"should fall back to settings endpoint"
);
assert.ok(
callOrder.some((u) => u.includes("/api/context/combos")),
"should fall back to combos endpoint"
);
});
test("compression engine set normalizes hybrid → stacked alias", async () => {
let captured: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) captured = JSON.parse(opts.body);
return Promise.resolve(makeResp({ success: true }));
}) as any;
await captureStdout(async () => {
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
await runCompressionEngineSet("hybrid", {}, makeCmd() as any);
});
globalThis.fetch = origFetch;
assert.equal(captured?.arguments?.engine, "stacked");
const first = callOrder[0] ?? "";
assert.ok(first.includes("/api/mcp/stream"), "should attempt MCP first");
assert.ok(callOrder.some((u) => u.includes("/api/settings/compression")), "should fall back to REST");
assert.ok(callOrder.some((u) => u.includes("/api/context/combos")), "should fetch combos");
assert.ok(callOrder.some((u) => u.includes("/api/context/analytics")), "should fetch analytics");
});
test("compression engine set falls back to PUT /api/settings/compression on MCP 404", async () => {
const calls: Array<{ url: string; method?: string; body?: any }> = [];
const calls: Array<{ url: string; method?: string; body?: unknown }> = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
globalThis.fetch = ((url: string, opts: unknown) => {
calls.push({
url,
method: opts?.method,
body: opts?.body ? JSON.parse(opts.body) : undefined,
});
if (url.includes("/api/mcp/tools/call")) {
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
if (url.includes("/api/mcp/stream")) {
const body = opts?.body ? JSON.parse(opts.body) : {};
if (body.method === "initialize") {
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
}
return Promise.resolve(makeMcpResp({ error: "not mounted" }, 404));
}
return Promise.resolve(makeResp({ ok: true }));
}) as any;
@@ -249,7 +263,6 @@ test("compression engine set falls back to PUT /api/settings/compression on MCP
const restCall = calls.find((c) => c.url.includes("/api/settings/compression"));
assert.ok(restCall, "should fall back to PUT /api/settings/compression");
assert.equal(restCall?.method, "PUT");
// #6571: the REST fallback now PUTs the canonical `defaultMode` field the server's
// strict schema accepts, not the nonexistent `engine` key (which made the PUT 400).
// #6571: the REST fallback now PUTs the canonical `defaultMode` field
assert.equal(restCall?.body?.defaultMode, "rtk");
});

View File

@@ -1,14 +1,16 @@
import test from "node:test";
import assert from "node:assert/strict";
function makeResp(data: unknown, status = 200) {
// ---- helpers ----
function makeResp(data: unknown, status = 200, extraHeaders: Record<string, string> = {}) {
const headers = new Headers({ "content-type": "application/json", ...extraHeaders });
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
headers,
};
obj.json = obj.json.bind(obj);
obj.text = obj.text.bind(obj);
@@ -34,116 +36,314 @@ function makeCmd(output = "json") {
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
}
test("mcp call envia name e arguments no body", async () => {
let capturedBody: any = null;
let capturedUrl = "";
// Simulate a /api/mcp/stream endpoint that speaks JSON-RPC 2.0
function makeMcpStreamFetch(
toolResult: { content: { type: string; text: string }[] } = {
content: [{ type: "text", text: "hello" }],
},
callStatus = 200,
) {
return ((url: string, opts: unknown) => {
const u = String(url);
if (!u.includes("/api/mcp/stream")) {
return Promise.resolve(makeResp({ error: "not found" }, 404));
}
const body = opts?.body ? JSON.parse(opts.body) : null;
// initialize
if (body && body.method === "initialize") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
200,
{ "mcp-session-id": "test-session-123" },
),
);
}
// tools/call
if (body && body.method === "tools/call") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 2, result: toolResult },
callStatus,
),
);
}
return Promise.resolve(makeResp({ error: "unknown method" }, 400));
}) as any;
}
// ---- tests ----
test("mcp call sends JSON-RPC initialize then tools/call", async () => {
const calls: Array<{ url: string; body: unknown }> = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
capturedUrl = url;
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ result: { health: "ok" } }));
globalThis.fetch = ((url: string, opts: unknown) => {
const u = String(url);
const body = opts?.body ? JSON.parse(opts.body) : null;
calls.push({ url: u, body });
if (body && body.method === "initialize") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
200,
{ "mcp-session-id": "sess-1" },
),
);
}
if (body && body.method === "tools/call") {
return Promise.resolve(
makeResp({
jsonrpc: "2.0",
id: 2,
result: { content: [{ type: "text", text: "ok" }] },
}),
);
}
return Promise.resolve(makeResp({ error: "unknown" }, 400));
}) as any;
// Simula o que runMcpCall faz internamente
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({ name: "omniroute_get_health", arguments: {} }),
try {
const { runMcpCallCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
const exitCode = await runMcpCallCommand(
"omniroute_get_health",
{},
{ stream: false },
{ baseUrl: "http://localhost:20128" },
);
assert.equal(exitCode, 0);
assert.equal(calls.length, 2);
assert.equal(calls[0].body.method, "initialize");
assert.equal(calls[1].body.method, "tools/call");
assert.equal(calls[1].body.params.name, "omniroute_get_health");
assert.deepEqual(calls[1].body.params.arguments, {});
} finally {
globalThis.fetch = origFetch;
}
});
test("mcp call passes session-id header on tools/call", async () => {
let callHeaders: Record<string, string> = {};
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: unknown) => {
const body = opts?.body ? JSON.parse(opts.body) : null;
if (body && body.method === "initialize") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
200,
{ "mcp-session-id": "sess-abc" },
),
);
}
if (body && body.method === "tools/call") {
callHeaders = opts.headers || {};
return Promise.resolve(
makeResp({
jsonrpc: "2.0",
id: 2,
result: { content: [{ type: "text", text: "ok" }] },
}),
);
}
return Promise.resolve(makeResp({ error: "unknown" }, 400));
}) as any;
try {
const { runMcpCallCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
const exitCode = await runMcpCallCommand(
"test_tool",
{ key: "val" },
{ stream: false },
{ baseUrl: "http://localhost:20128" },
);
assert.equal(exitCode, 0);
assert.equal(callHeaders["mcp-session-id"], "sess-abc");
} finally {
globalThis.fetch = origFetch;
}
});
test("mcp call prints result content to stdout", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = makeMcpStreamFetch({
content: [{ type: "text", text: "hello world" }],
});
const output = await captureStdout(async () => {
const { runMcpCallCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
await runMcpCallCommand(
"test",
{},
{ stream: false },
{ baseUrl: "http://localhost:20128" },
);
});
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
assert.equal(capturedBody.name, "omniroute_get_health");
assert.deepEqual(capturedBody.arguments, {});
assert.ok(output.includes("hello world"));
});
test("mcp call com --args passa argumentos como JSON", async () => {
let capturedBody: any = null;
test("mcp call prints error on non-ok response", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ result: {} }));
globalThis.fetch = ((url: string, opts: unknown) => {
const body = opts?.body ? JSON.parse(opts.body) : null;
if (body && body.method === "initialize") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
200,
{ "mcp-session-id": "sess-1" },
),
);
}
if (body && body.method === "tools/call") {
return Promise.resolve(makeResp({ error: "tool not found" }, 500));
}
return Promise.resolve(makeResp({ error: "unknown" }, 400));
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({ name: "omniroute_check_quota", arguments: { provider: "openai" } }),
try {
const { runMcpCallCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
const exitCode = await runMcpCallCommand(
"bad_tool",
{},
{ stream: false },
{ baseUrl: "http://localhost:20128" },
);
assert.equal(exitCode, 1);
} finally {
globalThis.fetch = origFetch;
}
});
test("mcp call with stream reads SSE data", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: unknown) => {
const body = opts?.body ? JSON.parse(opts.body) : null;
if (body && body.method === "initialize") {
return Promise.resolve(
makeResp(
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
200,
{ "mcp-session-id": "sess-stream" },
),
);
}
if (body && body.method === "tools/call") {
// Simulate an SSE stream via a ReadableStream body
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("data: stream-chunk-1\n\ndata: stream-chunk-2\n\n"));
controller.close();
},
});
return Promise.resolve({
ok: true,
status: 200,
body: stream,
headers: new Headers(),
json: () => Promise.reject(new Error("not json")),
text: () => Promise.reject(new Error("not text")),
});
}
return Promise.resolve(makeResp({ error: "unknown" }, 400));
}) as any;
const output = await captureStdout(async () => {
const { runMcpCallCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
await runMcpCallCommand(
"test",
{},
{ stream: true },
{ baseUrl: "http://localhost:20128" },
);
});
globalThis.fetch = origFetch;
assert.equal(capturedBody.arguments.provider, "openai");
assert.ok(output.includes("stream-chunk-1"));
assert.ok(output.includes("stream-chunk-2"));
});
test("mcp scopes envia meta=scopes na query", async () => {
let capturedUrl = "";
test("mcp status reads online field", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp({ scopes: ["read:health", "read:combos", "write:settings"] }));
globalThis.fetch = (async (_url: string | URL, init?: unknown) => {
const u = String(_url);
if (u.includes("/api/health")) {
return makeResp({ status: "ok" }) as any;
}
if (u.includes("/api/mcp/status")) {
return makeResp({
status: "online",
online: true,
transport: "stdio",
enabled: true,
toolsCount: 107,
}) as any;
}
return makeResp({ error: "not found" }, 404) as any;
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools?meta=scopes");
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("meta=scopes"));
});
test("mcp tools list busca /api/mcp/tools", async () => {
const TOOLS = [
{ name: "omniroute_get_health", scopes: ["read:health"], auditLevel: "low", phase: 1 },
{ name: "omniroute_list_combos", scopes: ["read:combos"], auditLevel: "low", phase: 1 },
];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string) => {
return Promise.resolve(makeResp({ tools: TOOLS }));
}) as any;
const out = await captureStdout(async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const res = await (globalThis.fetch as any)("/api/mcp/tools");
const data = await res.json();
emit(data.tools ?? data, makeCmd().optsWithGlobals());
const output = await captureStdout(async () => {
const { runMcpStatusCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
const exitCode = await runMcpStatusCommand({});
assert.equal(exitCode, 0);
});
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed.length, 2);
assert.ok(output.includes("MCP server running"), "should print running status, got: " + output);
assert.ok(output.includes("107"), "should print toolsCount");
});
test("mcp tools list com --scope filtra por scope", async () => {
let capturedUrl = "";
test("mcp status json mode prints full object", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp({ tools: [] }));
const u = String(url);
if (u.includes("/api/health")) {
return Promise.resolve(makeResp({ status: "ok" }, 200));
}
if (u.includes("/api/mcp/status")) {
return Promise.resolve(
makeResp({
status: "online",
online: true,
transport: "stdio",
enabled: true,
toolsCount: 107,
}),
);
}
return Promise.resolve(makeResp({ error: "not found" }, 404));
}) as any;
const params = new URLSearchParams({ scope: "read:health" });
await (globalThis.fetch as any)(`/api/mcp/tools?${params}`);
const output = await captureStdout(async () => {
const { runMcpStatusCommand } = await import(
"../../bin/cli/commands/mcp.mjs"
);
const exitCode = await runMcpStatusCommand({ json: true });
assert.equal(exitCode, 0);
});
globalThis.fetch = origFetch;
assert.ok(
capturedUrl.includes("scope=read%3Ahealth") || capturedUrl.includes("scope=read:health")
);
});
test("mcp audit stats passa period na query", async () => {
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp({ period: "30d", totalCalls: 500 }));
}) as any;
await (globalThis.fetch as any)("/api/mcp/audit/stats?period=30d");
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("period=30d"));
});
test("mcp.mjs pode ser importado sem erro", async () => {
const mod = await import("../../bin/cli/commands/mcp.mjs");
assert.equal(typeof mod.registerMcp, "function");
assert.equal(typeof mod.runMcpStatusCommand, "function");
assert.equal(typeof mod.runMcpRestartCommand, "function");
const parsed = JSON.parse(output.trim());
assert.equal(parsed.online, true);
assert.equal(parsed.toolsCount, 107);
});

View File

@@ -1,18 +1,9 @@
import test from "node:test";
import assert from "node:assert/strict";
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
function makeResp(data: unknown, status = 200) {
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
};
obj.json = obj.json.bind(obj);
obj.text = obj.text.bind(obj);
return obj;
return makeMcpResp(data, status) as any;
}
function makeCmd(output = "json") {
@@ -20,84 +11,47 @@ function makeCmd(output = "json") {
}
test("oneproxy status chama omniroute_oneproxy_stats via MCP", async () => {
let capturedBody: any = null;
const calls: any[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ poolSize: 10, activeProxies: 8 }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: { poolSize: 10, activeProxies: 8 } });
globalThis.fetch = (async (url: string, init?: any) => {
calls.push({ url: String(url), init });
return origFetch(url, init);
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({ name: "omniroute_oneproxy_stats", arguments: {} }),
});
await import("../../bin/cli/commands/oneproxy.mjs");
// ensure module registers; just assert stream mock shape
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_oneproxy_stats");
assert.ok(calls.length >= 0);
});
test("oneproxy stats passa provider e period para MCP", async () => {
let capturedBody: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ requests: 5000 }));
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_oneproxy_stats",
arguments: { provider: "openai", period: "24h" },
}),
});
globalThis.fetch = makeMcpStreamFetch({ toolResult: { requests: 5000 } });
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
const result = await mcpCallTool("omniroute_oneproxy_stats", { provider: "openai", period: "24h" });
globalThis.fetch = origFetch;
assert.equal(capturedBody.arguments.provider, "openai");
assert.equal(capturedBody.arguments.period, "24h");
assert.deepEqual(result, { requests: 5000 });
});
test("oneproxy fetch chama omniroute_oneproxy_fetch com count e type", async () => {
let capturedBody: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ proxies: [{ host: "10.0.0.1", type: "http" }] }));
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_oneproxy_fetch",
arguments: { count: 5, type: "http" },
}),
});
globalThis.fetch = makeMcpStreamFetch({ toolResult: { proxies: [{ host: "10.0.0.1", type: "http" }] } });
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
const result = await mcpCallTool("omniroute_oneproxy_fetch", { count: 5, type: "http" });
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_oneproxy_fetch");
assert.equal(capturedBody.arguments.count, 5);
assert.equal(capturedBody.arguments.type, "http");
assert.equal((result as any).proxies[0].host, "10.0.0.1");
assert.equal((result as any).proxies[0].type, "http");
});
test("oneproxy rotate chama omniroute_oneproxy_rotate com provider", async () => {
let capturedBody: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ rotated: true, newProxy: "10.0.0.2" }));
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_oneproxy_rotate",
arguments: { provider: "anthropic" },
}),
});
globalThis.fetch = makeMcpStreamFetch({ toolResult: { rotated: true, newProxy: "10.0.0.2" } });
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
const result = await mcpCallTool("omniroute_oneproxy_rotate", { provider: "anthropic" });
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_oneproxy_rotate");
assert.equal(capturedBody.arguments.provider, "anthropic");
assert.equal((result as any).rotated, true);
assert.equal((result as any).newProxy, "10.0.0.2");
});
test("oneproxy config set envia PUT /api/settings/oneproxy", async () => {

View File

@@ -1,4 +1,5 @@
import test from "node:test";
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
import assert from "node:assert/strict";
function makeResp(data: unknown, status = 200) {
@@ -111,25 +112,25 @@ test("resilience reset envia provider e body correto", async () => {
assert.equal(capturedBody.connectionId, "conn-1");
});
test("resilience profile set chama MCP tool", async () => {
let capturedBody: any = null;
test("resilience profile set usa JSON-RPC tools/call", async () => {
let capturedCall: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) capturedBody = JSON.parse(opts.body);
return Promise.resolve(makeResp({ result: {} }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
const inner = globalThis.fetch;
globalThis.fetch = ((url: any, init: any) => {
if (String(url).includes("/api/mcp/stream") && String(init?.body || "").includes("tools/call")) {
capturedCall = JSON.parse(init.body);
}
return inner(url, init);
}) as any;
await (globalThis.fetch as any)("/api/mcp/tools/call", {
method: "POST",
body: JSON.stringify({
name: "omniroute_set_resilience_profile",
arguments: { profile: "balanced" },
}),
});
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
await mcpCallTool("omniroute_set_resilience_profile", { profile: "balanced" });
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_set_resilience_profile");
assert.equal(capturedBody.arguments.profile, "balanced");
assert.equal(capturedCall.method, "tools/call");
assert.equal(capturedCall.params.name, "omniroute_set_resilience_profile");
assert.equal(capturedCall.params.arguments.profile, "balanced");
});
test("resilience.mjs pode ser importado sem erro", async () => {

View File

@@ -1,4 +1,5 @@
import test from "node:test";
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
import assert from "node:assert/strict";
const SKILLS_DATA = [
@@ -114,34 +115,37 @@ test("runSkillsGet busca /api/skills/:id", async () => {
assert.equal(parsed.id, "sk_pdf");
});
test("runSkillsEnable envia POST para tools/call", async () => {
let capturedUrl = "";
let capturedInit: any = null;
test("runSkillsEnable usa JSON-RPC tools/call", async () => {
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, init: any) => {
capturedUrl = url;
capturedInit = init;
return Promise.resolve(makeResp({ ok: true }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: { ok: true } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
const { runSkillsEnable } = await import("../../bin/cli/commands/skills.mjs");
const out = await captureStdout(() => runSkillsEnable("sk_pdf", {}, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
const body = JSON.parse(capturedInit?.body);
assert.equal(body.name, "omniroute_skills_enable");
assert.equal(body.arguments.skillId, "sk_pdf");
assert.equal(body.arguments.enabled, true);
assert.ok(calls.some((x) => String(x.url).includes("/api/mcp/stream")));
const callBody = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(callBody.method, "tools/call");
assert.equal(callBody.params.name, "omniroute_skills_enable");
assert.equal(callBody.params.arguments.skillId, "sk_pdf");
assert.equal(callBody.params.arguments.enabled, true);
assert.ok(out.includes("sk_pdf"));
});
test("runSkillsExecute envia POST com skillId e input", async () => {
let capturedBody: any = null;
test("runSkillsExecute usa JSON-RPC tools/call", async () => {
const calls: unknown[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, init: any) => {
capturedBody = JSON.parse(init.body);
return Promise.resolve(makeResp({ result: "ok", output: "parsed" }));
globalThis.fetch = makeMcpStreamFetch({ toolResult: { result: "ok", output: "parsed" } });
const inner = globalThis.fetch;
globalThis.fetch = ((url: unknown, init: unknown) => {
calls.push({ url: String(url), init });
return inner(url, init);
}) as any;
const { runSkillsExecute } = await import("../../bin/cli/commands/skills.mjs");
@@ -150,9 +154,11 @@ test("runSkillsExecute envia POST com skillId e input", async () => {
);
globalThis.fetch = origFetch;
assert.equal(capturedBody.name, "omniroute_skills_execute");
assert.equal(capturedBody.arguments.skillId, "sk_pdf");
assert.deepEqual(capturedBody.arguments.input, { file: "doc.pdf" });
const callBody = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
assert.equal(callBody.method, "tools/call");
assert.equal(callBody.params.name, "omniroute_skills_execute");
assert.equal(callBody.params.arguments.skillId, "sk_pdf");
assert.deepEqual(callBody.params.arguments.input, { file: "doc.pdf" });
});
test("runSkillsExecutions filtra por skill e status", async () => {
@@ -197,9 +203,9 @@ test("runMarketplaceSearch retorna pacotes com query e filtros", async () => {
});
test("runMarketplaceInstall --yes envia POST sem confirmação", async () => {
let capturedBody: any = null;
let capturedBody: unknown = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, init: any) => {
globalThis.fetch = ((_url: string, init: unknown) => {
capturedBody = JSON.parse(init?.body ?? "{}");
return Promise.resolve(makeResp({ skillId: "sk_pdf_installed" }));
}) as any;

View File

@@ -0,0 +1,49 @@
import type { Response as Resp } from "undici";
// Minimal fetch mock responses that satisfy what apiFetch needs.
export function makeMcpResp(data: unknown, status = 200, headers: Record<string, string> = {}) {
const hdrs = new Headers({ "content-type": "application/json", ...headers });
const obj = {
ok: status < 400,
status,
json: () => Promise.resolve(data),
text: () => Promise.resolve(typeof data === "string" ? data : JSON.stringify(data)),
headers: hdrs,
} as unknown as Resp;
return obj;
}
export function makeMcpStreamFetch({
toolResult = { content: [{ type: "text", text: "ok" }] },
initStatus = 200,
callStatus = 200,
callError = false,
} = {}) {
return (async (url: string | URL, init?: unknown) => {
const u = String(url);
if (!u.includes("/api/mcp/stream")) {
return makeMcpResp({ error: "not found" }, 404);
}
const body = init?.body ? JSON.parse(init.body) : {};
if (body.method === "initialize") {
return makeMcpResp(
{ jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2024-11-05", capabilities: {} } },
initStatus,
initStatus < 400 ? { "mcp-session-id": "sess-test" } : {},
);
}
if (body.method === "tools/call") {
if (callStatus !== 200) return makeMcpResp({ error: "tool failure" }, callStatus);
if (callError) {
return makeMcpResp({
jsonrpc: "2.0",
id: body.id,
result: { content: [{ type: "text", text: "tool error" }], isError: true },
});
}
return makeMcpResp({ jsonrpc: "2.0", id: body.id, result: toolResult });
}
return makeMcpResp({ error: "unknown method" }, 400);
}) as unknown as typeof globalThis.fetch;
}