// Вызов инструментов через настоящий MCP-протокол (in-memory транспорт) поверх // клиента с фейковым fetch на фикстурах: проверяет связку zod-схем, дефолтов, // кэша каталога и формы результатов. import { test } from "node:test"; import assert from "node:assert/strict"; import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { createTtlCache } from "../src/cache.js"; import { CATALOG_TTL_MS, buildCatalog } from "../src/catalog.js"; import { createClient } from "../src/client.js"; import { registerAllTools } from "../src/tools/index.js"; import { fixture } from "./helpers.js"; const KEY = "orr_live_Ab3dEfSECRETSECRETSECRETSECRET"; const FIXTURES: Record = { "/v1/models": "v1-models", "/api/models": "api-models", "/api/balance": "api-balance", "/api/usage": "api-usage", "/api/keys": "api-keys", }; async function setup(status = 200) { const counts: Record = {}; const fetchImpl = (async (input: string | URL | Request) => { const path = new URL(String(input)).pathname; counts[path] = (counts[path] ?? 0) + 1; if (status !== 200) return new Response(JSON.stringify({ error: { code: "invalid_key" } }), { status }); return new Response(JSON.stringify(fixture(FIXTURES[path] ?? "missing")), { status: 200 }); }) as typeof fetch; const client = createClient({ apiKey: KEY, baseUrl: "https://example.test" }, fetchImpl); const server = new McpServer({ name: "openrusrouter", version: "0.0.0" }); registerAllTools(server, { client, catalog: createTtlCache(CATALOG_TTL_MS, () => buildCatalog(client)), apiKey: KEY, }); const [a, b] = InMemoryTransport.createLinkedPair(); const mcp = new McpClient({ name: "t", version: "0" }); await Promise.all([server.connect(a), mcp.connect(b)]); return { mcp, counts }; } type Result = { isError?: boolean; structuredContent?: Record; content: { text: string }[] }; async function call(mcp: McpClient, name: string, args: Record = {}): Promise { return (await mcp.callTool({ name, arguments: args })) as unknown as Result; } test("list_models с дефолтами: только text, по цене, каталог кэшируется", async () => { const { mcp, counts } = await setup(); const r = await call(mcp, "list_models"); assert.notEqual(r.isError, true); const sc = r.structuredContent as { total: number; items: { id: string; modality: string }[] }; assert.equal(sc.total, 6); assert.ok(sc.items.every((m) => m.modality === "text")); assert.equal(sc.items[0]?.id, "nvidia/nemotron-3.5-super-vl:free"); assert.deepEqual(JSON.parse(r.content[0]?.text ?? ""), sc); await call(mcp, "list_models", { modality: "image", limit: 1, offset: 1 }); assert.equal(counts["/api/models"], 1); assert.equal(counts["/v1/models"], 1); }); test("pick_model и estimate_cost", async () => { const { mcp } = await setup(); const p = await call(mcp, "pick_model", { max_prompt_rub_1m: "9", limit: 1 }); const items = (p.structuredContent as { items: { id: string; why: string }[] }).items; assert.equal(items.length, 1); assert.equal(items[0]?.id, "nvidia/nemotron-3.5-super-vl:free"); const e = await call(mcp, "estimate_cost", { model_id: "openai/whisper-1", prompt_tokens: 10 }); assert.equal(e.isError, true); const missing = await call(mcp, "estimate_cost", { model_id: "no/such", prompt_tokens: 10 }); assert.equal(missing.isError, true); assert.match(missing.content[0]?.text ?? "", /не найдена в каталоге/); }); test("get_balance, get_usage, list_api_keys", async () => { const { mcp } = await setup(); const b = (await call(mcp, "get_balance")).structuredContent ?? {}; assert.equal(b.balance_rub, "1234.56789012"); assert.equal(b.referral_balance_rub, "0"); const u = (await call(mcp, "get_usage", { days: 7 })).structuredContent ?? {}; assert.equal(u.total_rub, "1.29459022"); const k = await call(mcp, "list_api_keys"); assert.ok(!(k.content[0]?.text ?? "").includes(KEY)); const keys = (k.structuredContent as { keys: { is_current_key: boolean }[] }).keys; assert.deepEqual(keys.map((x) => x.is_current_key), [true, false, false]); }); test("ошибка API превращается в isError без утечки ключа", async () => { const { mcp } = await setup(401); const r = await call(mcp, "get_balance"); assert.equal(r.isError, true); assert.match(r.content[0]?.text ?? "", /ключ недействителен/); assert.ok(!(r.content[0]?.text ?? "").includes(KEY)); });