MCP: api_get — read-only GET к API CRM для проверки эндпоинтов и структур ответов

ИИ-агент может до написания виджета проверить, что эндпоинт существует и сверить точные
имена полей (раньше выдумывал URL и shape — страницы не работали). Same-origin /api/*,
GET only, доступен в любом режиме ключа; запрос идёт от пользователя-владельца ключа.
This commit is contained in:
2026-09-30 17:51:22 +03:00
parent 49b7732a24
commit 8eff527259

View File

@@ -51,6 +51,7 @@ import { buildDataTableTree } from "./utils/data-table-tree";
import { sendTaskMessage, SendTaskMessageError } from "./services/task-message.service";
import { tasksMinimalCache, formsCache } from "./utils/cache";
import { evaluateAutoTransitions } from "./utils/auto-transitions";
import { signAccessToken } from "./utils/jwt";
import { notifyTaskAssigned } from "./utils/notifyAssignee";
import { eventBus, publishNotificationSSE, buildSystemFieldValues, buildTableRowMap, formatFieldValueForTitle, resolveTaskFieldTitles, type FieldTitleContext } from "./routes/shared";
import { indexFormAsync, indexTaskAsync } from "./routes/task-helpers";
@@ -134,6 +135,7 @@ const READ_TOOLS: readonly string[] = [
'get_custom_page',
'get_js_coding_reference',
'validate_custom_page_code',
'api_get',
'semantic_search',
'get_organization_context',
'list_directories',
@@ -1900,6 +1902,46 @@ RULES for tab component code:
}
);
// api_get — read-only GET к API CRM: проверка существования эндпоинта и точной структуры
// ответа ДО написания виджета (предотвращает выдуманные URL и поля). Доступен в любом режиме ключа.
register(
"api_get",
{
title: "API GET (read-only)",
description: `Perform an authenticated read-only GET against the CRM API and return { status, json }.
USE BEFORE WRITING WIDGET CODE: verify the endpoint EXISTS and inspect the EXACT JSON response shape —
do not invent URLs or field names. status 404 means the URL is wrong (check the spec);
a JSON shape different from your assumption means your parsing code is wrong.
url must start with /api/ (same-origin only, no external hosts). GET only.`,
inputSchema: {
url: z.string().regex(/^\/api\/[A-Za-z0-9\-/_]+(\?[^\s]*)?$/).describe("Path starting with /api/, e.g. /api/di2/nds/summary or /api/debt/analytics?page=1"),
},
},
async ({ url }) => {
const actor = await getActor();
const token = signAccessToken({
userId: actor.user.id,
organizationId,
appRole: actor.user.appRole ?? "user",
});
const port = process.env.PORT || 5000;
const r = await fetch(`http://127.0.0.1:${port}${url}`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
});
const text = await r.text();
let json: unknown;
try {
json = JSON.parse(text);
} catch {
json = text.slice(0, 500);
}
const body = JSON.stringify({ status: r.status, json }, null, 2);
return {
content: [{ type: "text" as const, text: body.length > 30000 ? body.slice(0, 30000) + "\n…(truncated)" : body }],
};
}
);
// validate_custom_page_code — статическая проверка кода ДО сохранения (read-скоуп, без мутаций)
register(
"validate_custom_page_code",