Добавь HTTP-клиент, каталог моделей и шесть read-only инструментов MCP

Клиент — замороженный объект с семью методами GET, без универсального
запроса и тела; денежные поля проверяются на строковый тип (fail-closed).
Каталог джойнит /api/models и /v1/models: модальность и рубли из первого,
USD и доступность ключу из второго. Инструменты: list_models, pick_model,
estimate_cost, get_balance, get_usage, list_api_keys. Точка входа stdio
уводит console.log/info в stderr первым импортом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vasyanskandClaude Opus 5.5 committed 2026-10-24 10:59:06 +07:00
1 parent ade6eb82df
commit d7d02267e2
18 files changed
+1935

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
// Собранный сервер отвечает на initialize и tools/list, а в stdout не
// пишет ничего, кроме JSON-RPC. В сеть не ходит: tools/list её не требует.
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync, spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
test("dist/index.js: stdout — только валидный JSON-RPC, шесть read-only инструментов", { timeout: 60_000 }, async () => {
execFileSync(process.execPath, ["node_modules/typescript/bin/tsc", "-p", "tsconfig.build.json"], {
cwd: root,
stdio: "inherit",
});
const child = spawn(process.execPath, ["dist/index.js"], {
cwd: root,
env: { PATH: process.env.PATH ?? "", OPENRUSROUTER_API_KEY: "test", OPENRUSROUTER_BASE_URL: "http://localhost:1" },
stdio: ["pipe", "pipe", "pipe"],
});
const lines: string[] = [];
let buf = "";
const waiters = new Map<number, (msg: Record<string, unknown>) => void>();
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
buf += chunk;
let i: number;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i);
buf = buf.slice(i + 1);
lines.push(line);
try {
const msg = JSON.parse(line) as Record<string, unknown>;
if (typeof msg.id === "number") waiters.get(msg.id)?.(msg);
} catch {
// невалидная строка будет поймана проверкой ниже
}
}
});
child.stderr.resume();
const send = (msg: object) => child.stdin.write(`${JSON.stringify(msg)}\n`);
const request = (id: number, method: string, params: object) =>
new Promise<Record<string, unknown>>((resolve) => {
waiters.set(id, resolve);
send({ jsonrpc: "2.0", id, method, params });
});
try {
const init = await request(1, "initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "test", version: "0" },
});
assert.equal((init.result as { serverInfo: { name: string } }).serverInfo.name, "openrusrouter");
send({ jsonrpc: "2.0", method: "notifications/initialized" });
const list = await request(2, "tools/list", {});
const tools = (list.result as { tools: { name: string; annotations?: Record<string, unknown> }[] }).tools;
assert.deepEqual(
tools.map((t) => t.name).sort(),
["estimate_cost", "get_balance", "get_usage", "list_api_keys", "list_models", "pick_model"],
);
for (const t of tools) {
assert.equal(t.annotations?.readOnlyHint, true, t.name);
assert.equal(t.annotations?.destructiveHint, false, t.name);
}
} finally {
child.kill();
}
assert.ok(lines.length >= 2);
for (const line of lines) {
assert.doesNotThrow(() => JSON.parse(line), `не JSON в stdout: ${line.slice(0, 100)}`);
}
});