mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
feat(cli): fase 5.1 — mcp call com stream e mcp scopes
This commit is contained in:
@@ -1,6 +1,26 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { apiFetch, isServerUp } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function truncate(v, len = 60) {
|
||||
if (v == null) return "-";
|
||||
const s = String(v);
|
||||
return s.length > len ? s.slice(0, len - 1) + "…" : s;
|
||||
}
|
||||
|
||||
const mcpToolSchema = [
|
||||
{ key: "name", header: "Tool", width: 36 },
|
||||
{
|
||||
key: "scopes",
|
||||
header: "Scopes",
|
||||
formatter: (v) => (Array.isArray(v) ? v.join(",") : (v ?? "-")),
|
||||
},
|
||||
{ key: "auditLevel", header: "Audit", width: 10 },
|
||||
{ key: "phase", header: "Phase", width: 6 },
|
||||
{ key: "description", header: "Description", formatter: truncate },
|
||||
];
|
||||
|
||||
export function registerMcp(program) {
|
||||
const mcp = program.command("mcp").description(t("mcp.title"));
|
||||
|
||||
@@ -22,6 +42,169 @@ export function registerMcp(program) {
|
||||
const exitCode = await runMcpRestartCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
// 5.1 — mcp call + mcp scopes
|
||||
mcp
|
||||
.command("call <tool> [argsJson]")
|
||||
.description(t("mcp.call.description"))
|
||||
.option("--args <json>", t("mcp.call.args"))
|
||||
.option("--args-file <path>", t("mcp.call.args_file"))
|
||||
.option("--stream", t("mcp.call.stream"))
|
||||
.option("--scope <s>", t("mcp.call.scope"), (v, prev = []) => [...prev, v], [])
|
||||
.action(async (tool, argsPositional, opts, cmd) => {
|
||||
const globalOpts = cmd.optsWithGlobals();
|
||||
const args = opts.args
|
||||
? JSON.parse(opts.args)
|
||||
: opts.argsFile
|
||||
? JSON.parse(readFileSync(opts.argsFile, "utf8"))
|
||||
: argsPositional
|
||||
? JSON.parse(argsPositional)
|
||||
: {};
|
||||
|
||||
if (opts.stream) {
|
||||
await runMcpStream(tool, args, globalOpts);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("scopes")
|
||||
.description(t("mcp.scopes.description"))
|
||||
.option("--tool <name>", t("mcp.scopes.tool"))
|
||||
.action(async (opts, cmd) => {
|
||||
const params = new URLSearchParams({ meta: "scopes" });
|
||||
if (opts.tool) params.set("tool", opts.tool);
|
||||
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.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) {
|
||||
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
|
||||
const apiKey = globalOpts.apiKey ?? "";
|
||||
const res = await fetch(`${baseUrl}/api/mcp/stream`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ name: tool, arguments: args }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`HTTP ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const reader = res.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMcpStatusCommand(opts = {}) {
|
||||
|
||||
@@ -244,7 +244,133 @@
|
||||
"title": "MCP Server",
|
||||
"running": "MCP server running ({transport})",
|
||||
"stopped": "MCP server stopped.",
|
||||
"restarted": "MCP server restarted."
|
||||
"restarted": "MCP server restarted.",
|
||||
"call": {
|
||||
"description": "Invoke an MCP tool directly",
|
||||
"args": "JSON arguments object (inline)",
|
||||
"args_file": "Path to JSON arguments file",
|
||||
"stream": "Use streaming endpoint (/api/mcp/stream)",
|
||||
"scope": "Required scope (repeatable, e.g. read:health)"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "List available MCP scopes",
|
||||
"tool": "Show scopes required by a specific tool"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Inspect MCP tools",
|
||||
"list": {
|
||||
"description": "List all MCP tools",
|
||||
"scope": "Filter by scope"
|
||||
},
|
||||
"info": {
|
||||
"description": "Show metadata for an MCP tool"
|
||||
},
|
||||
"schema": {
|
||||
"description": "Show input/output JSON schema for an MCP tool",
|
||||
"io": "Schema kind: input|output (default: input)"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"description": "MCP audit log (alias for audit --source mcp)"
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"skills": {
|
||||
"description": "List available A2A skills from the agent card"
|
||||
},
|
||||
"invoke": {
|
||||
"description": "Invoke an A2A skill and return the task ID",
|
||||
"input": "JSON input object",
|
||||
"input_file": "Path to JSON input file",
|
||||
"wait": "Wait for task completion before returning",
|
||||
"timeout": "Timeout waiting for completion in ms (default: 60000)"
|
||||
},
|
||||
"tasks": {
|
||||
"description": "Manage A2A tasks",
|
||||
"list": {
|
||||
"status": "Filter by status",
|
||||
"skill": "Filter by skill ID"
|
||||
},
|
||||
"watch": {
|
||||
"description": "Poll task status until completion"
|
||||
},
|
||||
"stream": {
|
||||
"description": "Stream task execution events via SSE"
|
||||
},
|
||||
"logs": {
|
||||
"description": "Show task messages and artifacts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"description": "Manage OmniRoute authorization policies",
|
||||
"list": {
|
||||
"description": "List policies",
|
||||
"kind": "Filter by kind (allow|deny|rate-limit|cost-cap)",
|
||||
"scope": "Filter by scope (global|api-key|provider)"
|
||||
},
|
||||
"get": {
|
||||
"description": "Get policy details by ID"
|
||||
},
|
||||
"create": {
|
||||
"description": "Create a policy from a JSON file",
|
||||
"file": "Path to policy JSON file"
|
||||
},
|
||||
"update": {
|
||||
"description": "Update a policy from a JSON file",
|
||||
"file": "Path to policy JSON file"
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete a policy by ID",
|
||||
"yes": "Skip confirmation prompt"
|
||||
},
|
||||
"evaluate": {
|
||||
"description": "Dry-run policy evaluation (exit 0=allowed, 4=denied)",
|
||||
"api_key": "API key to evaluate",
|
||||
"action": "Action to check (e.g. chat, embed, admin)",
|
||||
"resource": "Resource path or identifier",
|
||||
"context": "Additional context as JSON object"
|
||||
},
|
||||
"export": {
|
||||
"description": "Export all policies to a JSON file"
|
||||
},
|
||||
"import": {
|
||||
"description": "Import policies from a JSON file",
|
||||
"overwrite": "Overwrite existing policies with same ID"
|
||||
}
|
||||
},
|
||||
"compression": {
|
||||
"description": "Configure and inspect the OmniRoute compression pipeline",
|
||||
"status": {
|
||||
"description": "Show current compression status and settings"
|
||||
},
|
||||
"configure": {
|
||||
"description": "Configure compression settings",
|
||||
"engine": "Compression engine (caveman|rtk|hybrid|none)",
|
||||
"caveman_agg": "Caveman aggressiveness 0.0–1.0",
|
||||
"rtk_budget": "RTK token budget",
|
||||
"language_pack": "Language pack to activate"
|
||||
},
|
||||
"engine": {
|
||||
"description": "Get or set the active compression engine"
|
||||
},
|
||||
"combos": {
|
||||
"description": "Manage compression combo statistics"
|
||||
},
|
||||
"rules": {
|
||||
"description": "Manage compression rules",
|
||||
"add": {
|
||||
"pattern": "Pattern to match (regex or field:pattern)",
|
||||
"action": "Action: drop|shrink|replace"
|
||||
}
|
||||
},
|
||||
"language_packs": {
|
||||
"description": "List available compression language packs"
|
||||
},
|
||||
"preview": {
|
||||
"description": "Preview compression effect on a request",
|
||||
"file": "Path to request JSON file"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
"title": "Tunnels",
|
||||
|
||||
@@ -242,10 +242,136 @@
|
||||
},
|
||||
"mcp": {
|
||||
"title": "Servidor MCP",
|
||||
"call": {
|
||||
"description": "Invocar uma ferramenta MCP diretamente",
|
||||
"args": "Objeto JSON de argumentos (inline)",
|
||||
"args_file": "Caminho para arquivo JSON de argumentos",
|
||||
"stream": "Usar endpoint de streaming (/api/mcp/stream)",
|
||||
"scope": "Escopo requerido (repetível, ex: read:health)"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Listar escopos MCP disponíveis",
|
||||
"tool": "Mostrar escopos requeridos por uma ferramenta específica"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Inspecionar ferramentas MCP",
|
||||
"list": {
|
||||
"description": "Listar todas as ferramentas MCP",
|
||||
"scope": "Filtrar por escopo"
|
||||
},
|
||||
"info": {
|
||||
"description": "Exibir metadados de uma ferramenta MCP"
|
||||
},
|
||||
"schema": {
|
||||
"description": "Exibir schema JSON de entrada/saída de uma ferramenta MCP",
|
||||
"io": "Tipo de schema: input|output (padrão: input)"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"description": "Log de auditoria MCP (alias para audit --source mcp)"
|
||||
},
|
||||
"running": "Servidor MCP em execução ({transport})",
|
||||
"stopped": "Servidor MCP parado.",
|
||||
"restarted": "Servidor MCP reiniciado."
|
||||
},
|
||||
"a2a": {
|
||||
"skills": {
|
||||
"description": "Listar skills A2A disponíveis na agent card"
|
||||
},
|
||||
"invoke": {
|
||||
"description": "Invocar uma skill A2A e retornar o ID da task",
|
||||
"input": "Objeto JSON de entrada",
|
||||
"input_file": "Caminho para arquivo JSON de entrada",
|
||||
"wait": "Aguardar conclusão da task antes de retornar",
|
||||
"timeout": "Timeout aguardando conclusão em ms (padrão: 60000)"
|
||||
},
|
||||
"tasks": {
|
||||
"description": "Gerenciar tasks A2A",
|
||||
"list": {
|
||||
"status": "Filtrar por status",
|
||||
"skill": "Filtrar por ID de skill"
|
||||
},
|
||||
"watch": {
|
||||
"description": "Monitorar status da task até conclusão"
|
||||
},
|
||||
"stream": {
|
||||
"description": "Transmitir eventos de execução da task via SSE"
|
||||
},
|
||||
"logs": {
|
||||
"description": "Exibir mensagens e artefatos da task"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"description": "Gerenciar políticas de autorização do OmniRoute",
|
||||
"list": {
|
||||
"description": "Listar políticas",
|
||||
"kind": "Filtrar por tipo (allow|deny|rate-limit|cost-cap)",
|
||||
"scope": "Filtrar por escopo (global|api-key|provider)"
|
||||
},
|
||||
"get": {
|
||||
"description": "Obter detalhes de política por ID"
|
||||
},
|
||||
"create": {
|
||||
"description": "Criar política a partir de arquivo JSON",
|
||||
"file": "Caminho para arquivo JSON de política"
|
||||
},
|
||||
"update": {
|
||||
"description": "Atualizar política a partir de arquivo JSON",
|
||||
"file": "Caminho para arquivo JSON de política"
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletar política por ID",
|
||||
"yes": "Pular confirmação"
|
||||
},
|
||||
"evaluate": {
|
||||
"description": "Dry-run de avaliação de política (saída 0=permitido, 4=negado)",
|
||||
"api_key": "Chave de API a avaliar",
|
||||
"action": "Ação a verificar (ex: chat, embed, admin)",
|
||||
"resource": "Caminho ou identificador do recurso",
|
||||
"context": "Contexto adicional como objeto JSON"
|
||||
},
|
||||
"export": {
|
||||
"description": "Exportar todas as políticas para arquivo JSON"
|
||||
},
|
||||
"import": {
|
||||
"description": "Importar políticas de arquivo JSON",
|
||||
"overwrite": "Sobrescrever políticas existentes com mesmo ID"
|
||||
}
|
||||
},
|
||||
"compression": {
|
||||
"description": "Configurar e inspecionar o pipeline de compressão do OmniRoute",
|
||||
"status": {
|
||||
"description": "Exibir status atual de compressão e configurações"
|
||||
},
|
||||
"configure": {
|
||||
"description": "Configurar as definições de compressão",
|
||||
"engine": "Engine de compressão (caveman|rtk|hybrid|none)",
|
||||
"caveman_agg": "Agressividade do Caveman 0.0–1.0",
|
||||
"rtk_budget": "Budget de tokens RTK",
|
||||
"language_pack": "Language pack a ativar"
|
||||
},
|
||||
"engine": {
|
||||
"description": "Obter ou definir o engine de compressão ativo"
|
||||
},
|
||||
"combos": {
|
||||
"description": "Gerenciar estatísticas de combos de compressão"
|
||||
},
|
||||
"rules": {
|
||||
"description": "Gerenciar regras de compressão",
|
||||
"add": {
|
||||
"pattern": "Padrão para corresponder (regex ou campo:padrão)",
|
||||
"action": "Ação: drop|shrink|replace"
|
||||
}
|
||||
},
|
||||
"language_packs": {
|
||||
"description": "Listar language packs de compressão disponíveis"
|
||||
},
|
||||
"preview": {
|
||||
"description": "Visualizar efeito de compressão em uma requisição",
|
||||
"file": "Caminho para arquivo JSON da requisição"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
"title": "Túneis",
|
||||
"created": "Túnel criado: {url}",
|
||||
|
||||
149
tests/unit/cli-mcp-call-commands.test.ts
Normal file
149
tests/unit/cli-mcp-call-commands.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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;
|
||||
}
|
||||
|
||||
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
||||
const chunks: string[] = [];
|
||||
const orig = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (c: string | Uint8Array) => {
|
||||
chunks.push(typeof c === "string" ? c : c.toString());
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
process.stdout.write = orig;
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
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 = "";
|
||||
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" } }));
|
||||
}) 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: {} }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
|
||||
assert.equal(capturedBody.name, "omniroute_get_health");
|
||||
assert.deepEqual(capturedBody.arguments, {});
|
||||
});
|
||||
|
||||
test("mcp call com --args passa argumentos como JSON", 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({ result: {} }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "omniroute_check_quota", arguments: { provider: "openai" } }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.provider, "openai");
|
||||
});
|
||||
|
||||
test("mcp scopes envia meta=scopes na query", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ scopes: ["read:health", "read:combos", "write:settings"] }));
|
||||
}) 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());
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
const parsed = JSON.parse(out);
|
||||
assert.ok(Array.isArray(parsed));
|
||||
assert.equal(parsed.length, 2);
|
||||
});
|
||||
|
||||
test("mcp tools list com --scope filtra por scope", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ tools: [] }));
|
||||
}) as any;
|
||||
|
||||
const params = new URLSearchParams({ scope: "read:health" });
|
||||
await (globalThis.fetch as any)(`/api/mcp/tools?${params}`);
|
||||
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user