Servere MCP
CallAgent e client MCP: conectezi un server, tools-urile lui sunt descoperite, iar asistentul le poate apela ca pe orice alt tool. E și server MCP, deci Claude, Cursor sau orice alt client MCP poate administra o organizație direct.
Atașat întreg, nu desfăcut
Un server MCP se atașează unui asistent ca o singură legătură de tool, nu ca douăzeci de definiții separate lipite înăuntru. Contează din trei motive: lista de tools rămâne lizibilă, un server poate adăuga un tool fără ca cineva să editeze un asistent, și o singură politică de permisiuni acoperă tot ce expune serverul, în loc de douăzeci de politici care se despart în timp.
name: internalSystems
type: mcp
serverId: 01JQ…
permissionPolicy: allow_selected
allowedTools: [searchOrders, getShipment, listReturns]
confirmationRequiredFor: [destructive, financial, privileged]Intern, legătura e rezolvată în tools apelabile la pornirea apelului — modelul vede tools-urile individuale, tu administrezi un singur atașament.
API
GET /api/v1/mcp-servers
POST /api/v1/mcp-servers
GET /api/v1/mcp-servers/{mcpServer}
PATCH /api/v1/mcp-servers/{mcpServer}
DELETE /api/v1/mcp-servers/{mcpServer}
POST /api/v1/mcp-servers/{mcpServer}/test verificare de conectivitate
POST /api/v1/mcp-servers/{mcpServer}/discover recitește lista de tools
GET /api/v1/mcp-servers/{mcpServer}/tools
POST /api/v1/mcp-servers/{mcpServer}/tools/{toolName}/enabled
POST /api/v1/mcp-servers/{mcpServer}/tools/{toolName}/riskDetalii complete: referința serverelor MCP.
curl -X POST "$CALLAGENT_API_URL/mcp-servers" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sistem de comenzi",
"transport": "streamable_http",
"url": "https://mcp.example.com/",
"tool_policy": "allow_selected"
}'const auth = { Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}` };
const base = process.env.CALLAGENT_API_URL;
const server = await fetch(`${base}/mcp-servers`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Sistem de comenzi',
transport: 'streamable_http',
url: 'https://mcp.example.com/',
tool_policy: 'allow_selected',
}),
}).then((response) => response.json());
// Dovedește că funcționează înainte ca un asistent să depindă de el.
await fetch(`${base}/mcp-servers/${server.data.id}/test`, { method: 'POST', headers: auth });
await fetch(`${base}/mcp-servers/${server.data.id}/discover`, { method: 'POST', headers: auth });test și discover sunt separate intenționat: „serverul răspunde" și „astea sunt tools-urile lui azi" sunt fapte diferite, iar pe primul îl vrei înainte ca un asistent să depindă de al doilea.
Transporturi
| Transport | Când |
|---|---|
streamable_http | Transportul HTTP curent. De preferat. |
sse | HTTP vechi plus server-sent events. |
stdio | Un proces local. Doar pentru dezvoltare. |
Autentificare
Headere statice, un bearer token din depozitul de credențiale, sau OAuth prin managerul de conexiuni. Secretele sunt ținute ca credentialRef și rezolvate per apel — gateway-ul nu primește niciodată toată tabela de credențiale.
Niveluri de risc și confirmare
Fiecare tool descoperit e păstrat în cache cu un nivel de risc. Sunt cinci, și sunt același vocabular pe care îl folosesc tools-urile native, deci o singură politică de confirmare le acoperă pe ambele:
| Nivel | Înseamnă |
|---|---|
read | Citește informații. |
write | Schimbă starea. |
destructive | Șterge. |
financial | Cheltuie bani. |
privileged | Acționează cu autoritate ridicată. |
confirmation_policy de pe server ia trei valori:
| Valoare | Efect |
|---|---|
never | Nu întreabă niciodată. |
destructive | Implicit. Întreabă de la destructive în sus — deci și la financial și privileged. |
always | Întreabă pentru fiecare tool. |
Contractul are o politică mai bogată, pe care API-ul ăsta nu o expune
McpServerConfigSchema din @callagent/contracts are mode: never · always · risk_at_or_above · risk_in_set, cu threshold și riskSet. POST /mcp-servers acceptă doar cele trei de mai sus. Configurează prin API, cu cele trei.
Cele două se combină ca un prag minim, niciodată ca o reducere: ce calculează control plane-ul poate doar adăuga confirmare peste ce cere deja politica serverului. Nimic nu poate face în tăcere ca un tool să aibă nevoie de mai puțină confirmare decât a cerut serverul.
POST /mcp-servers/{id}/tools/{tool}/risk suprascrie un nivel declarat de server, iar POST /mcp-servers/{id}/tools/{tool}/enabled dezactivează un singur tool fără să deconecteze serverul.
Cache-ul de discovery
Câmpul e cache_ttl_seconds — secunde, minimum 60, maximum 86400, implicit 900 (15 minute). Fiecare citire îl limitează la cel puțin 60, deci nu există niciun fel de a opri cache-ul din API-ul ăsta. POST /mcp-servers/{id}/discover forțează o redescoperire acum, iar php artisan callagent:mcp:refresh-discovery redescoperă serverele al căror cache s-a învechit.
(discoveryTtlMs, și 0 însemnând „fără cache", există în configurația proprie de legare a tools-urilor din gateway. Nu sunt câmpul ăsta.)
Prompt injection
Rezultatele tools-urilor MCP sunt conținut necontrolat. Sunt împachetate înainte să ajungă la model, deci instrucțiunile dintr-un rezultat nu pot fi luate drept instrucțiuni de la tine. Fiecare rezultat de tool din contracte poartă untrusted: true ca literal — nu e un flag pe care să uite cineva să îl seteze.
Nu opri asta. Un server MCP pe care nu îl controlezi e, din punctul de vedere al modelului, un străin care scrie direct în conversație.
CallAgent ca server MCP
POST /mcp expune CallAgent însuși peste transportul streamable HTTP, autentificat cu o cheie de API obișnuită și delimitat de permisiunile cheii — un client MCP e un alt consumator de API, iar dacă i-ai da o cale privilegiată, cel mai puternic punct de intrare ar deveni cel mai puțin examinat.
GET /mcp întoarce 405 prin design: serverul ăsta nu trimite notificări nesolicitate, iar un client care ar ține un stream deschis pentru ele ar aștepta la nesfârșit.
Tools-urile expuse sunt listare și citire, plus câteva scrieri, fiecare protejată de scope-ul corespunzător:
| Tool | Scope |
|---|---|
assistant.list, assistant.get | assistant.read |
assistant.create, assistant.publish | assistant.write / assistant.publish |
tool.list, tool.create | tool.read / tool.write |
squad.list | squad.read |
phoneNumber.list | phone_number.read |
trunk.list | trunk.read |
campaign.list | campaign.read |
knowledgeBase.list | knowledge.read |
call.list, call.get | call.read |
Treisprezece tools, și asta e toată lista. Tools-urile al căror subsistem nu e terminat lipsesc, în loc să fie stub-uri — comentariul din registry spune că a face reclamă unuia e mai rău decât a-l omite. Deci nu există assistant.update, nu există call.create, nu există call.end, nu există phoneNumber.create, nu există analytics.query.
Îndreaptă un client MCP spre el cu o cheie delimitată exact la ce ar trebui să poată face clientul acela — o cheie doar-citire produce un asistent doar-citire.
Există două implementări; doar una e montată
packages/mcp conține un server MCP în TypeScript care declară 24 de tools. Nu îl importă nimic. E un schelet, fără niciun endpoint în vreuna dintre cele două aplicații.
Cel viu e serverul PHP de la POST /mcp, cu cele treisprezece tools de mai sus. Dacă găsești o listă de 24 de tools în sursă, aceea e scheletul.
Separat, feature flag-ul din seed pentru module.mcp_server încă spune not_implemented. Tratează flag-ul ca depășit, nu endpoint-ul ca absent; vezi Stadiul implementării.
Mai departe
- Tools — celelalte 24 de tipuri de tool.
- Securitate — de ce sunt împachetate rezultatele.