| name | apex-mcp-integration |
| description | Use when onboarding/registering a new MCP server into the APEX backend so the LLM agent can call its tools at runtime — deciding registry table (connector vs user), auth_mode (oauth2/api_key/internal), credential_scope (personal/company), KB seeder sync, and health-probe transport. Triggers on "add MCP integration", "onboard MCP server", "register tool-call MCP", "new vendor MCP", "MCP not showing in agent". |
Integrating a New MCP Server into APEX
Overview
This is the backend INTEGRATION layer — making a vendor/self-hosted MCP server's tools available to the LangGraph agent at runtime. It is NOT the outbound mcp/server.py wrapper (see apex-mcp-wrapper).
APEX splits MCP servers into two registries. Pick the right one first — everything else follows.
digraph pick {
"What is the MCP for?" [shape=diamond];
"Knowledge-base sync\n(scheduled → Vespa)" [shape=box];
"Agent calls it as a tool\n(per-turn, LLM-driven)" [shape=box];
"connector_mcp_servers\n+ add to KB_SERVICE_TYPES" [shape=box];
"user_mcp_servers" [shape=box];
"What is the MCP for?" -> "Knowledge-base sync\n(scheduled → Vespa)" [label="ingest docs"];
"What is the MCP for?" -> "Agent calls it as a tool\n(per-turn, LLM-driven)" [label="answer queries"];
"Knowledge-base sync\n(scheduled → Vespa)" -> "connector_mcp_servers\n+ add to KB_SERVICE_TYPES";
"Agent calls it as a tool\n(per-turn, LLM-driven)" -> "user_mcp_servers";
}
auth_mode matrix
auth_mode | Token source | Use for |
|---|
oauth2 | Per-user marketplace credential (mcp_user_credentials), lazy-refreshed | Vendor OAuth (Atlassian, MS Graph) |
api_key | Per-user credential blob → Bearer or service-specific header (e.g. figma → X-Figma-Token) | API-token vendors |
internal | Admin shared token via env_enc; no per-user token | Self-hosted / company-shared |
credential_scope: personal (user clicks Connect; visible in marketplace) vs company (admin token, hidden from marketplace UI).
Transport: APEX connects over HTTP — NOT stdio/command (L199)
APEX reaches every MCP server over the network. All bundled/connector MCPs use
transport_type streamable_http (jira/confluence/slack/o365/figma/bitbucket) or sse
(splunk), addressed by url or container_url (mcp_seeder.py). discover_server_tools
connects via container_url first, else url.
Do NOT register a new server with transport_type=stdio + command/args. That code
path (stdio_client, _build_stdio_params, the install_command/run_command install
family) still physically exists in schemas/mcp_server.py + mcp_runtime_shared.py but is
uncontracted legacy — it is not the onboarding pattern. The seeder proves it: zero
bundled MCPs use stdio.
APEX does NOT deploy or spawn the MCP server. Registration writes a DB row only
(url/container_url + transport + auth) — there is no "run this container" command in
the API (that was the abandoned stdio/command path, the ONLY path where APEX launched a
subprocess inside its own worker). For HTTP transports APEX merely connects to an
already-running endpoint. So a self-hosted MCP (like a FastMCP wrapper) must be deployed by
you — its own container/pod/host, reachable from APEX, ideally network-close to whatever
backend it wraps — running mcp.run(transport="streamable-http") + GET /health. Then
register url/container_url + transport_type=streamable_http. (container_url is just a
reachable URL — it does NOT mean APEX manages the container.) Secrets for HTTP servers go in
headers_enc; the wrapper may also auth downstream with its own env (CyberArk does).
End-to-end checklist (tool-call MCP)
- Registry: insert into
user_mcp_servers (tool-call) — via POST /api/mcp-servers or a seeder. KB sync server → connector_mcp_servers instead.
- If KB connector: add the
service_type to the SSOT set _CONNECTOR_MCPS / KB_SERVICE_TYPES in backend/app/shared/mcp_seeder.py AND keep the mcpsplit_001 migration SQL in sync — this set is read in multiple places (L180: do not hardcode elsewhere).
- auth_mode + credential_scope: choose per the matrix above.
- Health transport: set
transport_type correctly (streamable_http / sse / http for remote vendors; self-hosted exposes /health). The probe in mcp_health_monitor.py is transport-aware — remote vendor MCPs (e.g. mcp.atlassian.com) don't serve /health, so any non-5xx counts as healthy. Wrong transport → permanent unhealthy → agent filters it out.
- Activate:
is_active=true and scope in (both,agent_only), else it never enters the agent catalog.
- Cache flush: after DB toggle/cred change, delete Redis keys
mcp_tools*, mcp_catalog*, apex:mcp:* or tools won't refresh.
- Standalone wrapper: if you added a REST endpoint for this, mirror it in
mcp/server.py (see apex-mcp-wrapper).
Key files
| Concern | File |
|---|
| Split table models | backend/app/db/models_mcp_split.py |
| KB SSOT set | backend/app/shared/mcp_seeder.py (_CONNECTOR_MCPS → KB_SERVICE_TYPES) |
Token resolution / get_tools_for_bot / _INTERNAL_GATE_BY_SERVICE_TYPE | backend/app/services/mcp_client_manager.py |
discover_server_tools → MCPToolInfo[] → LangChain BaseTool[] | backend/app/services/mcp_runtime_shared.py |
Transport-aware health probe (_probe_one) | backend/app/shared/mcp_health_monitor.py |
credential_scope marketplace filter | backend/app/api/routes/marketplace.py |
| Register/CRUD routes | backend/app/api/routes/mcp_servers.py |
| Architecture doc | docs/SYSTEM.md (MCP / registry-split section) |
Line numbers drift — grep the symbol, don't trust an offset.
Common mistakes
| Mistake | Reality |
|---|
| Hardcoding KB vs tool-call set in a new place | Read KB_SERVICE_TYPES — 3-place hardcode caused L180 mis-seed |
Wrong transport_type for a remote vendor | Permanent unhealthy, agent drops the server |
| Expected tools after toggle, none appeared | Flush mcp_tools* Redis cache |
Assuming an onboard-agent endpoint exists | Onboard via POST /api/mcp-servers (+ draft-test) or a startup seeder — verify current routes |
| Editing only one table | Writes may dual-write legacy + split during transition — verify the current contract in models_mcp_split.py before assuming |
Verify (post-change smoke test)
Agent-path change → run the golden path (docs/runbooks/post-change-smoke-test.md): confirm the new server's tools appear via get_tools_for_bot(session, bot_uuid, slack_user_id) with an actor user set, and that the worker log shows the tool callable (not mcp_server_skipped_* / unhealthy).