MCP Model Bridge
# MCP Model Bridge
Runs an agent definition from `.github/agents/` against a model **you name**, instead of the one the IDE session happens to use.
Copilot agent definitions carry no `model` field, so a subagent inherits the session model. This server closes that gap: it reads the agent file, uses its body as the system prompt, and executes a bounded tool loop against a chosen model.
**Project-independent.** Nothing here is tied to the repository it was developed in. Point it at any project and it picks up that project's agents and that project's model policy.
## Status
MVP. Ships with a **mock gateway** so the wiring is testable without credentials. Point it at a real endpoint with two environment variables.
## Install
The bridge is a developer tool, not a runtime dependency. Install it once, use it everywhere.
```powershell
# from a checkout
npm install
npm run build
npm link
```
```powershell
# or straight from a git remote, once the package lives in its own repo
npm install -g git+https://github.com/<you>/mcp-model-bridge.git
```
```powershell
# or from a registry, if you publish it
npm install -g @jurulabs/mcp-model-bridge
```
All three put `mcp-model-bridge` on the PATH.
## Quick start
```powershell
npm run smoke
```
The smoke test spawns the built server against a fixture project under `test/fixture-workspace` that has nothing to do with the surrounding repository. It asserts that the bridge discovers that project's agents and policy, and that the same agent reports different models on different tiers.
Point it at a real project instead:
```powershell
node scripts/smoke-test.mjs C:\path\to\any\project
```
## Configuration
| Variable | Default | Purpose |
|---|---|---|
| `BRIDGE_WORKSPACE_ROOT` | `process.cwd()` | The host project. Root for path confinement and tool access. |
| `BRIDGE_AGENTS_DIR` | `<root>/.github/agents` | Where agent definitions live |
| `BRIDGE_MODEL_POLICY` | auto-discovered | Tier map and allow-list |
| `BRIDGE_GATEWAY_URL` | *(unset → mock)* | Endpoint base URL |
| `BRIDGE_GATEWAY_TOKEN` | *(unset → mock)* | Credential, read from the environment only |
| `BRIDGE_GATEWAY_SCHEMA` | `anthropic-messages` | `anthropic-messages` or `openai-chat` |
| `BRIDGE_GATEWAY_AUTH` | per schema | `x-api-key` or `bearer` |
In practice only `BRIDGE_WORKSPACE_ROOT` needs setting. Everything else is discovered.
With `BRIDGE_GATEWAY_URL` and `BRIDGE_GATEWAY_TOKEN` unset, the server runs the mock gateway and reports `gateway: mock`. A half-configured gateway also reports `mock`, which surfaces the missing variable instead of hiding it behind a confident label.
## Where the gateway values come from
**Not from GitHub Copilot.** The Copilot subscription has no endpoint you can post arbitrary requests to, so it cannot serve as the gateway. The two values come from an inference provider you hold credentials with, billed separately from Copilot. That second cost centre is the price of choosing the model yourself.
The client speaks two request shapes, selected by `BRIDGE_GATEWAY_SCHEMA`:
| Schema | Path appended to the base URL | Default auth |
|---|---|---|
| `anthropic-messages` *(default)* | `/v1/messages` | `x-api-key` |
| `openai-chat` | `/v1/chat/completions` | `bearer` |
A base URL that already ends in `/v1` is not doubled, so whichever form the provider documents can be pasted as-is.
### OpenRouter — one key, every model
The best fit for this bridge, because the tier idea only pays off when `cheap`, `default` and `strong` can point at **different vendors** behind a single credential.
| Variable | Value |
|---|---|
| `BRIDGE_GATEWAY_URL` | `https://openrouter.ai/api/v1` |
| `BRIDGE_GATEWAY_TOKEN` | a key from *openrouter.ai → Keys* |
| `BRIDGE_GATEWAY_SCHEMA` | `openai-chat` |
| `BRIDGE_GATEWAY_AUTH` | leave unset — `openai-chat` already defaults to `bearer` |
OpenRouter is prepaid: top up credits, then spend per token. Model identifiers are vendor-prefixed, `anthropic/…`, `openai/…`, `google/…`. List the current ones and their prices before writing the policy:
```powershell
(Invoke-RestMethod https://openrouter.ai/api/v1/models).data |
Select-Object id, @{n='in';e={$_.pricing.prompt}}, @{n='out';e={$_.pricing.completion}} |
Sort-Object in
```
Two caveats worth knowing up front. OpenRouter may route a model across several upstream providers with differing context limits and latency — pin one with the `:nitro` or provider-routing suffixes if that matters. And the bridge's text tool protocol is unaffected either way, since it never relies on native tool calling.
### Anthropic directly
| Variable | Value |
|---|---|
| `BRIDGE_GATEWAY_URL` | `https://api.anthropic.com` |
| `BRIDGE_GATEWAY_TOKEN` | a key from *console.anthropic.com → Settings → API Keys* |
| `BRIDGE_GATEWAY_SCHEMA` | leave unset |
| `BRIDGE_GATEWAY_AUTH` | leave unset — Anthropic expects `x-api-key` |
Needs a paid Anthropic account with credit; the key is shown once at creation.
### A corporate or self-hosted gateway
| Variable | Value |
|---|---|
| `BRIDGE_GATEWAY_URL` | the base path the gateway documents |
| `BRIDGE_GATEWAY_TOKEN` | issued by that gateway's portal, not by the model vendor |
| `BRIDGE_GATEWAY_SCHEMA` | whichever shape it exposes |
| `BRIDGE_GATEWAY_AUTH` | usually `bearer` — these normally sit behind an OAuth proxy |
Confirm in the gateway's own documentation which schema it speaks and which model identifiers it accepts.
### A translating proxy
Run LiteLLM locally and it fronts OpenAI, Azure OpenAI, Bedrock or Vertex behind either schema. `BRIDGE_GATEWAY_URL` = `http://localhost:4000`, token = the master key you set in the proxy config, auth `bearer`. The upstream provider key then lives in the proxy config, not in this bridge.
### Model identifiers are the usual trip-hazard
The bundled policy names Copilot picker labels such as `claude-sonnet-5`. **No provider API recognises those.** Replace them with the identifiers your gateway accepts, in the host project's `.github/model-policy.json`. For OpenRouter that means vendor-prefixed ids:
```json
{
"tiers": {
"cheap": "<vendor>/<small model>",
"default": "<vendor>/<mid model>",
"strong": "<vendor>/<large model>"
},
"allowedModels": ["<vendor>/<small model>", "<vendor>/<mid model>", "<vendor>/<large model>"]
}
```
A model outside the allow-list is refused, so a stale identifier fails at once rather than after a billed call.
### Supplying the values
Per session:
```powershell
$env:BRIDGE_GATEWAY_URL = 'https://openrouter.ai/api/v1'
$env:BRIDGE_GATEWAY_TOKEN = '<key>'
$env:BRIDGE_GATEWAY_SCHEMA = 'openai-chat'
```
Persistently for your user account:
```powershell
[Environment]::SetEnvironmentVariable('BRIDGE_GATEWAY_TOKEN', '<key>', 'User')
```
Never in the repository, never in the agent file, never in the policy file.
### Proving the gateway answers
The smoke test only exercises the mock. One real call, costing one short completion:
```powershell
npm run gateway:check -- <model-id>
```
It prints the resolved endpoint, schema and auth scheme, then the model's own reply. The three things that can only fail live — credentials, schema, model identifier — all surface here rather than mid-session.
Inside the IDE, `list_agents` reports the same resolution without ever echoing the token:
```
gateway: http (https://openrouter.ai/api/v1/chat/completions, schema: openai-chat, auth: bearer)
```
`gateway: mock` means one of the two required variables did not reach the process.
## Model policy
The policy file holds the tier map and the allow-list:
```json
{
"tiers": { "cheap": "gpt-5.6-luna", "default": "claude-sonnet-5", "strong": "claude-opus-5" },
"allowedModels": ["gpt-5.6-luna", "claude-sonnet-5", "claude-opus-5"]
}
```
It is looked up in this order:
1. `BRIDGE_MODEL_POLICY`, if set
2. `<workspaceRoot>/.github/model-policy.json` — the host project's own policy
3. the bundled `model-policy.json` — so a fresh project needs no setup
`list_agents` reports which one won, as `source: explicit | workspace | bundled`.
A model outside `allowedModels` is **refused with an explicit error**. Silent fallback to an expensive model is the failure mode that costs money without anyone noticing, so it is not offered.
## Tools
### `run_agent`
| Parameter | Required | Meaning |
|---|---|---|
| `agent` | yes | File stem under the agents directory, e.g. `Explorer` |
| `task` | yes | The instruction, including the desired output contract |
| `model` | no | Tier name (`cheap`, `default`, `strong`) or an allow-listed model id |
| `maxRounds` | no | Tool rounds before stopping, default 4, ceiling 10 |
Returns the agent's output followed by a provenance block:
```
agent: Explorer
model used: gpt-5.6-luna (tier: cheap)
rounds: 2, tool calls: 1
stopped: completed
```
The provenance block is the point of the exercise. A bridge that runs but reports the session model has failed at the only thing it exists to do.
### `list_agents`
Reports the agents the host project defines, the agents directory, the active policy file and where it came from, the tier map, the allow-list, and whether the gateway is live or mocked.
```
agents directory: C:\some\project\.github\agents
agents: Explorer, Reviewer, ShopifyApi, Tester
model policy: C:\some\project\.github\model-policy.json (source: workspace)
tiers: cheap -> gpt-5.6-luna, default -> claude-sonnet-5, strong -> claude-opus-5
allowed models: gpt-5.6-luna, claude-sonnet-5, claude-opus-5
gateway: mock
```
A project with no agents directory reports `agents: (none found)` rather than failing, so the tool can still explain the rest of the configuration.
## Registering in JetBrains
*Settings → Tools → GitHub Copilot → Model Context Protocol (MCP)*
```json
{
"servers": {
"model-bridge": {
"command": "mcp-model-bridge",
"env": {
"BRIDGE_WORKSPACE_ROOT": "C:\\path\\to\\the\\project\\you\\are\\working\\in"
}
}
}
}
```
If you did not link the binary onto the PATH, call the entry point directly instead:
```json
{
"command": "node",
"args": ["C:\\path\\to\\mcp-model-bridge\\dist\\index.js"]
}
```
Run `npm run build` first — the entry point is the compiled `dist/index.js`.
## Safety
- **Read-only toolset.** `read_file` and `grep_search` only; a cheap model driving these tools cannot edit anything.
- **Path confinement.** Every requested path resolves against the workspace root; traversal is refused.
- **Round ceiling.** The loop cannot run away, so it cannot spend without limit.
- **Credentials from the environment.** Never from the agent file or the policy file.
- **Separate billing.** Gateway calls are charged outside the Copilot subscription. Two cost centres exist; only one appears in the IDE.
## Architecture
The full design, including the variants considered and why this scope was chosen, lives in the global `ai-automation` skill:
```
~/.agents/skills/ai-automation/MCP-BRIDGE.md
```
To move this package into its own repository, see [EXTRACTION.md](./EXTRACTION.md).
## Known limits
- Agent bodies written for richer toolsets degrade here. An agent that relies on `semantic_search` falls back to `grep_search`.
- Two upstream schemas are implemented, Anthropic Messages and OpenAI Chat Completions. A third shape is a one-file change in `src/gateway-client.ts`, or a translating proxy in front of it.
- Tool calls travel as fenced ` ```tool ` blocks rather than a native tool-calling API, which keeps the bridge portable at the cost of some reliability on weaker models.
- Publishing under the `@jurulabs` scope requires owning that scope on the target registry. Git and `npm link` installs need no registry at all.
TDQS
Scored across 2 tools
list_agents is a read-only introspection operation and run_agent is an execution operation, targeting entirely different actions on the same domain. There is no plausible way for an agent to confuse the two.
Both tools follow a clean verb_noun snake_case pattern (list_agents, run_agent) with the noun 'agent' consistent across the set. Fully predictable.
Two tools is thin even for a narrow bridge surface; a discovery call plus a single execution call leaves little room for inspecting, validating, or managing runs. Still, both tools clearly earn their place given the stated purpose.
The pairing of discover-agents and execute-agent covers the core read/run lifecycle, and run_agent returns the model actually used, closing the routing-verification loop. Gaps are minor and largely handled outside the server (agent definitions live in the host project), though there is no status, cancel, or batch execution path.