Server Usage¶
The gateway ships an OpenAI-compatible HTTP API, so any OpenAI SDK client works against it unchanged.
Start the server¶
Or programmatically:
import uvicorn
from mcp_for_copilot.gateway.app import create_app
app = create_app()
uvicorn.run(app, host="0.0.0.0", port=8080)
Routes¶
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Liveness probe |
GET |
/v1/models |
OpenAI-shaped model list |
GET |
/v1/status |
Configuration summary (booleans only) |
POST |
/v1/chat/completions |
Chat completions, streaming and non-streaming |
POST |
/v1/debug/marathon |
Queue up to 10 prioritized debug tasks (202) |
GET |
/v1/debug/marathon/{job_id} |
Read queue, progress, and results |
GET /health¶
GET /v1/models¶
{
"object": "list",
"data": [
{"id": "gpt-6-astra", "object": "model", "owned_by": "openai"},
{"id": "gpt-6-sol", "object": "model", "owned_by": "openai"},
{"id": "gpt-6-luna", "object": "model", "owned_by": "openai"},
{"id": "gpt-5.6-sol", "object": "model", "owned_by": "openai"},
{"id": "gpt-5.6-luna", "object": "model", "owned_by": "openai"}
]
}
POST /v1/chat/completions¶
curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-6-sol",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": false
}'
Streaming:
curl -N http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-6-sol",
"messages": [{"role": "user", "content": "Count to five."}],
"stream": true
}'
x_gateway metadata¶
Non-streaming responses carry an x_gateway object describing what the orchestrator did:
| Field | Type | Meaning |
|---|---|---|
rounds |
int |
Number of model round-trips |
used_tools |
bool |
Whether any tool was executed |
invocations |
list |
Every tool call attempted, with allow/approve/execute flags |
plan |
list |
The model's working plan, empty unless MCP_PLAN_MODE is on |
plan_progress |
dict |
Count of plan steps per status |
Debug marathon¶
Submit up to 10 tasks. Each score is an integer from 1 to 5. Tasks are ordered by priority,
difficulty, then complexity, highest first. The response includes a job_id; poll the status route
to follow the sequential run.
curl -s http://localhost:8080/v1/debug/marathon \
-H "Content-Type: application/json" \
-d '{
"tasks": [
{"title": "Simple regression", "question": "Find the null handling bug", "priority": 3, "difficulty": 2, "complexity": 2},
{"title": "Race condition", "question": "Diagnose the intermittent write race", "priority": 5, "difficulty": 5, "complexity": 5}
]
}'
curl -s http://localhost:8080/v1/debug/marathon/<job_id>
The target task mix is 80% Luna-only (difficulty/complexity 1-2), 15% Luna then Sol (3-4), and 5% the full Luna -> Sol -> Astra ladder (5). These are target proportions, not enforced quotas; uncertain outputs and provider failures escalate to the next model. At equal token volume, catalog prices are approximately 1:20:100 for Luna:Sol:Astra, so this target mix is about 10 Luna-cost units per task versus 121 units when every task uses all three stages. Actual spend depends on prompt and answer lengths and the incoming task mix.
completed means the final model self-reported SOLVED; the gateway does not execute code or tests.
The status response reports processed, completed, model_reported_success_rate, token usage, and
catalog-based estimated_cost_usd. Measure accepted fixes or passing tests on a representative
benchmark before claiming an actual success rate above 80%. Job state is in process memory and is
lost when the gateway restarts.
Each task and the job itself also carry a verification field. It is not_executed unless a stage was
actually handed execution output, in which case it is executed. The default is deliberately
pessimistic: a model claiming success does not set executed. Tasks additionally carry a plan
list parsed from the model's MARATHON_PLAN line.
Status codes¶
| Code | When |
|---|---|
200 |
Success |
202 |
Debug marathon accepted and queued |
400 |
messages missing, empty, or not a list; unknown model |
401 |
Gateway API key is missing or invalid |
404 |
Debug marathon job ID is unknown or no longer retained |
422 |
Invalid debug marathon payload or score |
502 |
The upstream provider failed (see GatewayResult.error) |
A provider failure is reported as 502 rather than 500 because the gateway itself is healthy — the
upstream is not.
Docker¶
Build¶
The image is multi-stage: a builder stage compiles the wheel, and a slim runtime stage installs it as a
non-root user (uid 10001) with a HEALTHCHECK against /health.
Run¶
docker run --rm -p 8080:8080 \
-e LLM_API_KEY="sk-..." \
-e LLM_MODEL_ID="gpt-6-sol" \
mcp-for-copilot:latest
Compose¶
docker-compose.yml defines two services:
| Service | Role |
|---|---|
gateway |
The FastAPI server on port 8080 |
mcp |
A stdio MCP server the gateway connects to |
Both declare healthchecks, and gateway waits for mcp to become healthy before starting.
Reverse proxy notes¶
- Streaming uses
text/event-stream. Disable response buffering in nginx (proxy_buffering off;) or the stream will arrive in one burst. - Set generous read timeouts. A tool-calling round trip can take tens of seconds.
- The gateway does not terminate TLS. Put it behind a proxy that does.
Production checklist¶
-
LLM_API_KEYinjected from a secret manager, never baked into the image -
MCP_ALLOWED_TOOLSrestricted to the minimum set the workload needs - An approver configured, or risky tools removed from the allowlist entirely
-
MCP_MAX_TOOL_ROUNDSset low enough to bound cost - Logs shipped to a collector that respects the built-in redaction
-
/healthwired to your orchestrator's liveness probe