MCP konektory: nacte nastroje ze serveru a udela z nich kroky
Firma si zalozi napojeni na svuj MCP server, stiskne Nacist nastroje a jeho
nastroje se objevi v builderu jako kroky automatizace vcetne toho, jake
promenne prijimaji a jake vraceji.
Pribylo:
- sluzba `mcp` - jedina v katalogu bez pevnych operaci, rekne je az server.
Udaje: adresa serveru, token nebo klic v X-API-Key
- POST /connectors/:id/mcp/tools - zepta se serveru na tools/list a ulozi
vysledek. Je to zaroven overeni konektoru, proto u MCP neni tlacitko Overit
- src/mcp/client.ts - handshake, sezeni z hlavicky odpovedi, odpoved jako JSON
i jako SSE stream, strankovani nastroju, nic z toho nevyhazuje vyjimku
- src/mcp/schema.ts - ze schematu vzniknou pole kroku a zpatky se z vyplnenych
retezcu udelaji argumenty ve spravnych typech. Ten druhy smer je ten
podstatny: server ceka {"limit": 10}, ne {"limit": "10"}
- src/data/mcpTools.ts - nastroje v katalogu, kes nad tim, co je u konektoru
- sloupec `mcp` u konektoru (migrace 004). Bez ulozeni by po restartu zmizely
z katalogu kroky, ktere uzivatel uz ma ve stromech
- vnitrni krok runMcpTool - jedna obsluha pro vsechny nastroje vsech serveru
Rozhodnuti:
- nastroj patri firme, ne katalogu. serviceCatalog(tenantId) bez firmy nevrati
zadny, takze zapomenuty argument znamena "nic", ne "vsechno"
- ID operace nese ID konektoru (tool:<konektor>:<nastroj>), protoze firma muze
mit dva servery a na obou nastroj `search`
- krok se neopakuje, MCP nema idempotencni klic
- chyba nemaze nastroje, vypadek serveru nesmi vymazat kroky z automatizaci
- servery se pri startu neobvolavaji, jeden nedostupny by shodil katalog vsem
Dokumentace: prepsany 24-mcp-konektory.md na skutecny stav, novy
00-pro-programatory.md (rozcestnik, model ctyr pojmu, pravidla, ktera plati
vsude, co je krehke), doplnene 01, 12 a 99.
Mimochodem opraveno: setStatus v connectors/postgres.ts melo v RETURNING
doslovny retezec ${COLUMNS} misto dosazeni, a dva odstavce v dokumentu 12 byly
dvakrat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3e6b365eec
commit
435e254c90
+156
-1
@@ -26,6 +26,7 @@ import {
|
||||
getConnector,
|
||||
listConnectors,
|
||||
setConnectorStatus,
|
||||
setConnectorTools,
|
||||
toPublicConnector,
|
||||
updateConnector,
|
||||
validateConnectorValues,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
import {
|
||||
canSeeService,
|
||||
findService,
|
||||
MCP_SERVICE_ID,
|
||||
serviceCatalog,
|
||||
serviceCategories,
|
||||
visibleServices,
|
||||
@@ -42,6 +44,8 @@ import {
|
||||
import { config } from '../config.js';
|
||||
import { egressIp } from '../data/egressIp.js';
|
||||
import { smtpSettings, smtpTargetUrl, verifySmtp } from '../mail/smtp.js';
|
||||
import { listTools } from '../mcp/client.js';
|
||||
import { forgetMcpTools, rememberMcpTools } from '../data/mcpTools.js';
|
||||
import { resolveTarget, serviceBaseUrl, targetSecrets } from '../scripts/connections.js';
|
||||
import { createHttp } from '../scripts/http.js';
|
||||
import { ScriptError } from '../scripts/types.js';
|
||||
@@ -101,7 +105,10 @@ connectorsRouter.get('/services', async (req, res) => {
|
||||
const tenantId = requested ?? access.defaultTenantId;
|
||||
|
||||
const visible = visibleServices(req.user!, tenantId);
|
||||
const withScripts = new Map(serviceCatalog().map((service) => [service.id, service]));
|
||||
// Firma se predava kvuli MCP: nastroje jsou vlastnost jejiho napojeni.
|
||||
const withScripts = new Map(
|
||||
serviceCatalog(tenantId ?? null).map((service) => [service.id, service]),
|
||||
);
|
||||
const counts = tenantId
|
||||
? await connectorCountsByService([tenantId])
|
||||
: new Map<string, number>();
|
||||
@@ -253,6 +260,9 @@ connectorsRouter.patch('/:id', async (req, res) => {
|
||||
if (!updated) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
// Nazev konektoru je v nazvu kazdeho jeho nastroje ve vyberu kroku. Bez
|
||||
// tohohle by tam po prejmenovani zustal stary az do restartu.
|
||||
if (updated.serviceId === MCP_SERVICE_ID) rememberMcpTools(updated);
|
||||
return res.json(toPublicConnector(updated));
|
||||
});
|
||||
|
||||
@@ -263,9 +273,104 @@ connectorsRouter.delete('/:id', async (req, res) => {
|
||||
if (!(await deleteConnector(req.params.id, [tenantId]))) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
// Smazanym konektorem zmizi i jeho nastroje z katalogu, jinak by v builderu
|
||||
// zustaly kroky, ktere uz nemaji kam volat.
|
||||
forgetMcpTools(req.params.id);
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------- MCP
|
||||
|
||||
/**
|
||||
* Zepta se MCP serveru na nastroje a ulozi je ke konektoru.
|
||||
*
|
||||
* Jedno misto pro dve cesty: tlacitko Nacist nastroje i overeni konektoru
|
||||
* delaji u MCP totez. Kdyby to bylo dvakrat, jedno by casem umelo neco navic.
|
||||
*
|
||||
* Ulozi se **i prazdny vysledek**: server, ktery uz zadny nastroj nenabizi, ma
|
||||
* v katalogu zmizet. Pri chybe se naopak nemeni nic - vypadek serveru nesmi
|
||||
* vymazat kroky z automatizaci, ktere uzivatel uz ma postavene.
|
||||
*/
|
||||
async function loadMcpTools(connectorId: string, tenantId: string) {
|
||||
const connector = await getConnector(connectorId, [tenantId]);
|
||||
if (!connector) return null;
|
||||
|
||||
const target = resolveTarget(connector.serviceId, connector);
|
||||
const checked = 'nástroje serveru';
|
||||
|
||||
if (!target.ready) {
|
||||
const message = `Napojení není hotové: ${target.missing.join(', ')}.`;
|
||||
await setConnectorStatus(
|
||||
connector.id,
|
||||
'error',
|
||||
message,
|
||||
{
|
||||
at: new Date().toISOString(),
|
||||
ok: false,
|
||||
checked: 'nic',
|
||||
status: null,
|
||||
message,
|
||||
detail: null,
|
||||
request: null,
|
||||
responseHeaders: null,
|
||||
egressIp: null,
|
||||
},
|
||||
[tenantId],
|
||||
);
|
||||
return { ok: false, checked: 'nic', message, tools: [] };
|
||||
}
|
||||
|
||||
const outcome = await listTools(target);
|
||||
|
||||
await setConnectorStatus(
|
||||
connector.id,
|
||||
outcome.ok ? 'ok' : 'error',
|
||||
outcome.ok ? null : outcome.message,
|
||||
{
|
||||
at: new Date().toISOString(),
|
||||
ok: outcome.ok,
|
||||
checked,
|
||||
status: outcome.status,
|
||||
message: outcome.message,
|
||||
detail: outcome.detail,
|
||||
request: outcome.request,
|
||||
responseHeaders: null,
|
||||
// Server si zaklada firma, seznamy povolenych IP na nem nemame v ruce.
|
||||
egressIp: null,
|
||||
},
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
if (!outcome.ok || !outcome.value) {
|
||||
console.warn(`[mcp] ${connector.id}: ${outcome.message}`);
|
||||
return {
|
||||
ok: false,
|
||||
checked,
|
||||
message: outcome.message,
|
||||
...(outcome.status !== null ? { status: outcome.status } : {}),
|
||||
...(outcome.detail ? { detail: outcome.detail } : {}),
|
||||
request: outcome.request,
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
|
||||
const saved = await setConnectorTools(connector.id, outcome.value, [tenantId]);
|
||||
if (saved) rememberMcpTools(saved);
|
||||
|
||||
const publicView = saved ? toPublicConnector(saved) : null;
|
||||
console.info(`[mcp] ${connector.id}: ${outcome.message}`);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checked,
|
||||
message: outcome.message,
|
||||
request: outcome.request,
|
||||
server: outcome.value.server,
|
||||
protocolVersion: outcome.value.protocolVersion,
|
||||
tools: publicView?.mcp?.tools ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- overeni
|
||||
|
||||
/**
|
||||
@@ -314,6 +419,18 @@ connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
return res.json({ ok: false, checked: 'nic', message, baseUrl: target.baseUrl });
|
||||
}
|
||||
|
||||
/*
|
||||
* MCP se overuje tim, ze si rekne o nastroje. Jina cteci operace v protokolu
|
||||
* neni a `/health` by u nej nedavalo smysl - MCP server zadne nema.
|
||||
*/
|
||||
if (service.transport === 'mcp') {
|
||||
const outcome = await loadMcpTools(connector.id, tenantId);
|
||||
if (!outcome) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
return res.json({ ...outcome, baseUrl: target.serviceConfig.serverUrl ?? '' });
|
||||
}
|
||||
|
||||
/*
|
||||
* SMTP se neoveruje ctecim volanim, ale prihlasenim. `verify` nic neposila,
|
||||
* takze test nikomu nic nedorucí - a pritom bez platneho hesla neprojde,
|
||||
@@ -468,6 +585,44 @@ connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Nacteni nastroju MCP serveru.
|
||||
*
|
||||
* Tohle je ta cast, kterou ma MCP navic proti ostatnim sluzbam. Jinde je
|
||||
* seznam operaci nas kod, tady ho rekne az server: portal se zepta `tools/list`
|
||||
* a z odpovedi vzniknou kroky vcetne toho, jake promenne prijimaji a jake
|
||||
* vraceji.
|
||||
*
|
||||
* Je to zaroven **overeni konektoru**, proto se zapisuje i do historie: kdyz
|
||||
* server odpovi seznamem, adresa i token sedi. Cteci volani, nic nemeni,
|
||||
* da se spustit kdykoliv.
|
||||
*/
|
||||
connectorsRouter.post('/:id/mcp/tools', async (req, res) => {
|
||||
const tenantId = tenantOrDeny(req, res);
|
||||
if (!tenantId) return;
|
||||
|
||||
const connector = await getConnector(req.params.id, [tenantId]);
|
||||
if (!connector) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
const service = serviceOrDeny(req, res, connector.serviceId, tenantId);
|
||||
if (!service) return;
|
||||
|
||||
if (service.transport !== 'mcp') {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: `${service.name} není MCP server, nástroje nemá odkud načíst.`,
|
||||
});
|
||||
}
|
||||
|
||||
const outcome = await loadMcpTools(connector.id, tenantId);
|
||||
if (!outcome) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
// Neuspesne nacteni neni chyba API, je to vysledek. Proto 200.
|
||||
return res.json(outcome);
|
||||
});
|
||||
|
||||
/**
|
||||
* Historie overeni konektoru.
|
||||
*
|
||||
|
||||
@@ -903,7 +903,9 @@ dashboardRouter.get('/services', (req, res) => {
|
||||
res.json({
|
||||
categories: serviceCategories,
|
||||
items: withRuntimeOptions(
|
||||
serviceCatalog().filter((service) => visible.has(service.id)),
|
||||
// Firma se predava kvuli MCP: nastroje jsou vlastnost jejiho napojeni,
|
||||
// ne katalogu. Bez ni se nevrati zadne.
|
||||
serviceCatalog(tenantId ?? null).filter((service) => visible.has(service.id)),
|
||||
{
|
||||
people: listPeople(tenantIds).map((person) => ({ id: person.id, name: person.name })),
|
||||
groups: listGroups(tenantIds).map((group) => ({ id: group.id, name: group.name })),
|
||||
|
||||
Reference in New Issue
Block a user