Library Usage¶
The library surface is the Gateway facade plus the lower-level building blocks, all re-exported from
the top-level package.
The Gateway facade¶
Gateway owns the whole stack: settings, provider, MCP client, router, approval layer, and
orchestrator. Use it as an async context manager so the MCP connection is closed deterministically.
import asyncio
from mcp_for_copilot import Gateway
async def main() -> None:
async with Gateway(connect_mcp=False) as gateway:
result = await gateway.chat([{"role": "user", "content": "Hello!"}])
print(result.content)
asyncio.run(main())
Constructor arguments¶
| Argument | Type | Default | Meaning |
|---|---|---|---|
settings |
Settings \| None |
None |
Explicit settings; otherwise loaded from env |
provider |
LLMProvider \| None |
None |
Inject a provider (used in tests) |
approver |
Approver \| None |
None |
Approval callback; None means deny all risky calls |
connect_mcp |
bool |
True |
Set False for pure-LLM use |
The MCP endpoint, transport, and allowlist come from Settings (MCP_SERVER_URL,
MCP_TRANSPORT, MCP_ALLOWED_TOOLS), not from constructor keywords.
Methods¶
| Method | Returns | Notes |
|---|---|---|
await gateway.chat(messages, *, system=None, model_id=None, max_rounds=None) |
GatewayResult |
The main entry point |
await gateway.start() |
None |
Called automatically by __aenter__ |
await gateway.stop() |
None |
Called automatically by __aexit__ |
await gateway.list_tools() |
list[dict] |
OpenAI-shaped tool schemas |
gateway.status() |
dict |
Booleans only, never credential values |
gateway.log_summary() |
None |
Logs the effective configuration |
GatewayResult¶
| Field | Type | Meaning |
|---|---|---|
content |
str |
The model's final answer |
model |
str |
Resolved model id |
rounds |
int |
Number of model round-trips |
used_tools |
bool |
Whether any tool was executed |
invocations |
list[ToolInvocation] |
Every tool call attempted |
plan |
list[dict] |
The model's working plan ({"description", "status"}), empty unless plan mode is on |
plan_progress |
dict |
Count of steps per status (pending/in_progress/done/blocked) |
usage |
dict |
Token accounting when the provider reports it |
error |
str |
Non-empty when the provider failed |
Plan mode¶
Set MCP_PLAN_MODE=true (or pass settings with mcp_plan_mode=True) to let the model keep a short
working plan. The orchestrator then advertises a synthetic update_plan tool and asks for at most six
steps, each pending, in_progress, done, or blocked, with exactly one step in_progress.
The plan is advisory only. update_plan is intercepted inside the orchestrator: it never reaches
the allowlist, the approval layer, or a back-end MCP server, so turning plan mode on cannot widen what
the model may execute. The plan is reset at the start of every chat() call, so one turn never
inherits another's plan.
from mcp_for_copilot import Gateway, get_settings
settings = get_settings().model_copy(update={"mcp_plan_mode": True})
async with Gateway(settings=settings) as gateway:
result = await gateway.chat([{"role": "user", "content": "Refactor the parser"}])
for step in result.plan:
print(step["status"], step["description"])
print(result.plan_progress) # {'pending': 2, 'in_progress': 1, 'done': 3, 'blocked': 0}
ToolInvocation¶
| Field | Type | Meaning |
|---|---|---|
tool |
str |
Tool name |
arguments |
dict |
Arguments the model supplied |
allowed |
bool |
Passed the allowlist and risk gates |
approved |
bool |
Passed the approval gate |
executed |
bool |
Actually invoked on the MCP server |
is_error |
bool |
The MCP server reported an error |
result |
str |
Tool output, or the denial reason |
reason |
str |
Why the call was allowed or denied |
Custom approvers¶
An approver is any callable that takes an ApprovalRequest and returns an ApprovalDecision.
from mcp_for_copilot import ApprovalDecision, ApprovalRequest
def my_approver(request: ApprovalRequest) -> ApprovalDecision:
print(f"Tool: {request.tool}")
print(f"Args: {request.arguments}")
print(f"Why: {request.reason}")
answer = input("Allow? [y/N] ").strip().lower()
return ApprovalDecision(approved=answer == "y", reason="operator decision")
ApprovalRequest carries tool, arguments, reason, and round_index.
Three factories ship with the package:
| Factory | Behaviour |
|---|---|
allow_all_approver() |
Approves everything. Use only in trusted automation. |
deny_all_approver() |
Denies everything. Useful for dry runs. |
static_approver(allow=(...), deny=(...)) |
Keyword-only allow/deny lists by tool name. |
from mcp_for_copilot import static_approver
approver = static_approver(allow=("read_file",), deny=("write_file",))
Fail-closed by design
If the approver raises, or returns a non-ApprovalDecision, the call is denied. There is no
path where an exception in your approver silently grants access.
Tool routing¶
ToolRouter is usable on its own if you want to reuse the safety logic elsewhere.
from mcp_for_copilot import ToolRouter, is_risky_tool
print(is_risky_tool("delete_file")) # True
print(is_risky_tool("read_file")) # False
router = ToolRouter(allowed_tools=("read_file", "list_directory"))
decision = router.route(tool_call, advertised_tools)
print(decision.allowed, decision.reason)
Passing an empty tuple means "allow nothing". Passing None means "use the built-in read-only
defaults". This distinction matters when you want a locked-down deployment.
Using the orchestrator directly¶
from mcp_for_copilot import LLMMCPOrchestrator, LLMProvider, MCPClient
provider = LLMProvider()
client = MCPClient(url="python -m my_mcp_server", transport="stdio")
orchestrator = LLMMCPOrchestrator(provider=provider, mcp_client=client)
result = await orchestrator.run([{"role": "user", "content": "List the files."}])
print(result.content, result.error)
Model resolution¶
from mcp_for_copilot import resolve_model_id, aliases_for, KNOWN_MODELS
print(resolve_model_id("sol")) # gpt-6-sol
print(resolve_model_id("gpt6")) # gpt-6-sol
print(aliases_for("gpt-6-sol")) # ('gpt-6', 'gpt6', 'sol', ...)
print(sorted(KNOWN_MODELS)) # all five model ids
Testing your integration¶
Gateway(connect_mcp=False) plus a hand-assigned fake client is the supported injection pattern:
This avoids spawning a subprocess in unit tests while still exercising the real orchestrator.