Architecture¶
Component map¶
flowchart TB
subgraph Entry["Entry points"]
CLI["cli.py<br/>mcp-for-copilot"]
APP["gateway/app.py<br/>FastAPI"]
SRV["gateway/server.py<br/>MCP stdio server"]
end
subgraph Core["Core"]
FAC["gateway/facade.py<br/>Gateway"]
ORC["orchestrator.py<br/>LLMMCPOrchestrator"]
RTR["tool_router.py<br/>ToolRouter"]
APR["approval.py<br/>ApprovalLayer"]
PRV["provider.py<br/>LLMProvider"]
MCP["mcp_client.py<br/>MCPClient"]
MGR["mcp_client.py<br/>MCPClientManager"]
CFG["config.py<br/>Settings"]
LOG["logging_utils.py<br/>redact()"]
end
CLI --> FAC
CLI --> SRV
APP --> FAC
SRV --> FAC
FAC --> ORC
FAC --> MCP
FAC --> MGR
ORC --> PRV
ORC --> RTR
ORC --> APR
ORC --> MCP
FAC --> CFG
ORC --> CFG
PRV --> CFG
MCP --> CFG
MGR --> CFG
PRV --> LOG
MCP --> LOG
ORC --> LOG
Gateway holds either an MCPClient (single server, from MCP_SERVER_URL) or an MCPClientManager
(many servers, from MCP_SERVERS). The manager fans out list_tools() and call_tool() across every
connected server and drops servers that fail to connect at startup.
Request lifecycle¶
sequenceDiagram
participant U as User
participant G as Gateway
participant O as Orchestrator
participant L as GPT6-SOL
participant R as ToolRouter
participant A as ApprovalLayer
participant M as MCP Server
U->>G: chat("list the files")
G->>O: run(messages)
O->>M: list_tools()
M-->>O: [read_file, list_directory, ...]
O->>L: messages + tool schemas
L-->>O: tool_calls: [list_directory({path: "."})]
loop each tool call
O->>R: route(call)
R-->>O: allowed / denied
O->>A: request_approval(call)
A-->>O: approved / denied
O->>M: call_tool(name, args)
M-->>O: result
end
O->>L: messages + tool results
L-->>O: final answer
O-->>G: OrchestratorResult
G-->>U: GatewayResult
The three gates¶
Every tool call must pass all three gates before it reaches the MCP server.
Gate 1 — Allowlist¶
ToolRouter only permits tools that are both named in the allowlist and advertised by the
connected MCP server. A tool that is allowlisted by name but never advertised is denied, because the
gateway cannot validate its arguments without a schema.
Gate 2 — Risk classification¶
is_risky_tool() matches the tool name against 40 destructive verbs (delete, drop, write,
execute, deploy, truncate, …) plus an explicit risky set (codex_run, mongo_aggregate) and an
explicit safe set that overrides the pattern match.
Arguments are validated against the tool's JSON schema before execution: required keys present, types match, no unexpected keys.
Gate 3 — Approval¶
ApprovalLayer is fail-closed. It denies when:
- no approver is configured,
- the approver raises an exception,
- the approver returns something that is not an
ApprovalDecision.
A denial is returned to the model as a tool error, so the model can explain the refusal instead of the request crashing.
Provider quirk handling¶
The GPT-6 family rejects reasoning_effort combined with function tools on /v1/chat/completions.
The gateway detects the presence of tools and forces reasoning_effort = "none" on the request body.
The field must be present and equal to "none" — omitting it is also rejected.
Error propagation¶
LLMProviderError is never raised out of LLMMCPOrchestrator.run(). Instead it is recorded in
OrchestratorResult.error, which Gateway copies into GatewayResult.error. The HTTP layer then
decides the status code — 502 Bad Gateway for provider failures, 400 for bad input. This keeps the
library usable without an HTTP context while still letting the server return correct status codes.
Streaming¶
Streaming is not incremental at the provider level. The orchestrator produces the complete answer
(including any tool round-trips), then _stream() chunks it at 220 characters to match OpenAI's
server-sent-event wire format. This keeps tool calling and streaming compatible, at the cost of
time-to-first-token.
Module responsibilities¶
| Module | Responsibility |
|---|---|
config.py |
Environment loading, validation, Settings.describe() |
logging_utils.py |
Secret redaction, structured log helpers |
provider.py |
Model catalogue, aliases, OpenAI client wrapper |
mcp_client.py |
MCP transports, tool discovery, tool invocation |
tool_router.py |
Allowlist, risk classification, argument validation |
approval.py |
Human-in-the-loop gate, fail-closed |
orchestrator.py |
The bounded tool-calling loop |
gateway/facade.py |
Lifecycle owner, ergonomic chat() |
gateway/app.py |
OpenAI-compatible HTTP surface |
gateway/server.py |
MCP stdio server exposing the gateway |
cli.py |
serve, mcp, chat, models, status |