Клиент — замороженный объект с семью методами 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>
62 lines
3.0 KiB
TypeScript
62 lines
3.0 KiB
TypeScript
// Инструмент list_api_keys — только чтение списка ключей аккаунта.
|
||
|
||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||
import { z } from "zod";
|
||
import type { ApiKeyEntry, Client } from "../client.js";
|
||
import { cleanText } from "../sanitize.js";
|
||
import { READ_ONLY_ANNOTATIONS, guarded, okResult } from "./common.js";
|
||
|
||
export const KEYS_TOOL_DESCRIPTION =
|
||
"Список API-ключей аккаунта openrusrouter.ru: префикс, лимит расхода и его окно, израсходовано, разрешённые модели. " +
|
||
"Полный ключ не выводится никогда, только префикс; is_current_key отмечает ключ, которым работает этот MCP. " +
|
||
"allowed_models: null вместе с all_models_allowed: true означает, что ключу доступны все модели. " +
|
||
"Создать, отозвать или изменить ключ через MCP нельзя — такие действия требуют браузерной сессии на openrusrouter.ru. " +
|
||
"Это сознательное ограничение безопасности: ключ, которым пользуется MCP, не должен уметь управлять другими ключами или собой";
|
||
|
||
export function presentKeys(
|
||
keys: readonly ApiKeyEntry[],
|
||
currentApiKey: string,
|
||
includeRevoked: boolean,
|
||
): Record<string, unknown>[] {
|
||
return keys
|
||
.filter((k) => includeRevoked || k.revoked_at === null)
|
||
.map((k) => {
|
||
const allowed = Array.isArray(k.allowed_models) && k.allowed_models.length > 0 ? k.allowed_models : null;
|
||
return {
|
||
id: k.id,
|
||
name: cleanText(typeof k.name === "string" ? k.name : "", 200),
|
||
key_prefix: k.prefix,
|
||
created_at: k.created_at,
|
||
last_used_at: k.last_used_at ?? null,
|
||
revoked_at: k.revoked_at ?? null,
|
||
spend_window: k.spend_window === "" ? null : k.spend_window,
|
||
spend_limit_rub: k.spend_limit_rub,
|
||
spend_used_rub: k.spend_used,
|
||
spend_resets_at: k.spend_resets_at ?? null,
|
||
allowed_models: allowed === null ? null : allowed.map((m) => cleanText(String(m), 200)),
|
||
all_models_allowed: allowed === null,
|
||
// Сравнение локальное: сам ключ в результат не попадает.
|
||
is_current_key: k.prefix !== "" && currentApiKey.startsWith(k.prefix),
|
||
};
|
||
});
|
||
}
|
||
|
||
export function registerKeyTools(server: McpServer, client: Client, currentApiKey: string): void {
|
||
server.registerTool(
|
||
"list_api_keys",
|
||
{
|
||
title: "API-ключи аккаунта",
|
||
description: KEYS_TOOL_DESCRIPTION,
|
||
inputSchema: {
|
||
include_revoked: z.boolean().default(false),
|
||
},
|
||
annotations: READ_ONLY_ANNOTATIONS,
|
||
},
|
||
(args) =>
|
||
guarded(async () => {
|
||
const { keys } = await client.getKeys();
|
||
return okResult({ keys: presentKeys(keys, currentApiKey, args.include_revoked) });
|
||
}),
|
||
);
|
||
}
|