Credentialed-tool MCP server¶
The credentialed-MCP server is a streamable-HTTP MCP endpoint mounted on the Litestar API app that exposes a scoped, governed subset of the credential-holding tools (forge / chat, with connections and remote-git following the same pattern) to an embedded coding harness. It is the second governance boundary behind the embedded OpenHands loop; the first is the LLM gateway.
The design invariant is credentials never enter the sandbox. Tool execution, credential brokering, the approval gate, the action signature and the egress pin all run host-side in the API process; the harness only ever sees tool schemas and already-fenced tool results.
Boundary¶
Base forge/chat tools are agent-engine BaseTools over
GovernedConnectionTool, not MCP handlers, and the in-process meta/mcp
machinery has no live network transport. This boundary adds one
(api/mcp_gateway/controller.py) at POST /mcp-gateway/mcp, so the full
mounted route (under the app's /api/v1 prefix) is
/api/v1/mcp-gateway/mcp; the harness's MCP base_url is the app address
plus /api/v1/mcp-gateway (the runtime appends /mcp). It is reachable
only from the sandbox over the sidecar egress allowlist and authenticated
with the same per-run signed bearer as the gateway. It is excluded from
the session/bearer auth middleware for that reason.
Tool set and capabilities¶
api/mcp_gateway/tools.py declares the credentialed tools as a static
tuple, each carrying a capability tag:
- forge (
forge:read/forge:write): repo, issue, pull-request, CI. - chat (
chat:read/chat:write): messages, directory.
Each tool wraps the existing governed implementation
(tools/forge/forge_tools.py, tools/chat/chat_tools.py) rather than
re-implementing it.
Scoping (per actor)¶
The bearer's capability list scopes visibility. visible_tool_names
resolves a tool with denied name > allowed name > capability-pattern
match, so an operator can widen by capability and still carve out an
individual tool by name. tools/list returns only visible schemas;
tools/call on a scoped-out name is a ResourceNotFoundError (invisible)
or ForbiddenError (visible but not permitted), never a silent success.
Per-call governance (host-side)¶
invoke_credentialed_tool runs, in order:
- Scope check against the actor's capabilities (above).
- Security pre-check: the controller wires a fail-closed
security_pre_checkthat runs the SecOps pre-tool evaluation (rule engine + audit + escalation) viaSecOpsService.evaluate_pre_tool; a non-ALLOWverdict denies the call, and with no active security governance every credentialed call is denied (the credentialed tools are unreachable until security is enabled). - Typed boundary:
parse_typedover the raw MCPargumentsinto the tool's args model; a bad shape is a typedValidationError, never a crash. - Governed dispatch: the tool's
GovernedConnectionTool.executepipeline runs theConnectionApprovalGate(anActionSignatureSHA-256-binds namespace + connection + operation + payload), parks aPENDINGapproval when unapproved, brokers the credential via theConnectionCataloghost-side, builds a per-call client pinned to the connectionbase_url(a structural egress pin), and dispatches. Destructive tools additionally invokerequire_admin_guardrails(confirm + reason + actor) as their first statement. - SEC-1 at source: the tool output is wrapped with
wrap_untrusted(TAG_TOOL_RESULT, ...)before it returns to the harness, so untrusted upstream content is fenced where it originates.
Approval-parking UX¶
When the approval gate parks a call, the harness receives a
"requires approval" observation, not a failure. The correct harness
behaviour is to pause and re-issue the identical call after a human
approves; because the ActionSignature is deterministic over the same
arguments, the re-issued call matches the approved signature and proceeds.
This is a first-class flow, documented so the adapter treats it as
pause-and-retry rather than a terminal error.
Protocol¶
api/mcp_gateway/protocol.py::dispatch_mcp handles initialize,
tools/list and tools/call. A tool error becomes an MCP isError
result (not a transport error); an unknown method is -32601; malformed
params are -32602. Batch requests are supported.
Settings¶
Under the tools namespace, off by default, hot-reloadable:
credentialed_mcp_enabled, credentialed_mcp_capabilities (the
comma-separated default capability grant), and credentialed_mcp_base_url
(the sandbox-reachable base URL the harness connects to, e.g.
http://host.internal:8000/api/v1/mcp-gateway; the runtime appends
/mcp). It reuses the existing forge/chat connection, timeout, and
read-limit settings. Both the enable toggle and any widening of
credentialed_mcp_capabilities (a broader grant, e.g. "" -> "*" or
adding a :write) open a credentialed egress path, so they route through
the confirm+reason+actor guardrail in settings/write_governance.py.
Wiring¶
An auto-discovered feature registers CredentialedMcpController alongside
the gateway controller. The controller resolves its per-call context
(connection catalog, approval store, forge/chat connection names,
timeouts, security pre-check) from app_state at request time, so
tools.* toggles take effect on the next request with no restart. All
collaborators already exist on app_state (connection catalog via the
integrations slice, approval_store_of, app_state.clock); no new state
slice is introduced.
Convention gate¶
scripts/check_credentialed_mcp_governed.py enforces that every
credentialed-MCP tool routes through the governed connection pipeline and
that invoke_credentialed_tool wraps outputs via wrap_untrusted.