Skip to main content
Glama
README.md
# mcp-copilot

A local MCP (Model Context Protocol) server that bridges prompts to **Microsoft Copilot** by driving your already signed-in Edge browser. Kiro, VS Code (Copilot Chat / Claude Code) and any other MCP client can call Copilot as a native tool — no API key, no separate login.

```
Kiro / VS Code → MCP HTTP :3100 → copilot-bridge (host Node.js or Docker) → CDP → your Edge → Copilot
```

> **The bridge must be running and listening on port 3100 for any editor to connect.**
> Verify: `curl http://localhost:3100/health` → `{"status":"ok",...,"auth":"ready"}`.
> If it is stopped, Kiro/VS Code show the server as disconnected and no tools appear.
> Start it with `scripts/run.sh` (macOS/Linux) or `scripts\run.bat` (Windows) and choose Node.js or Docker. In Docker mode it uses `restart: unless-stopped`, so it comes back after Docker restarts.

Architecture, design choices and editor setup: [docs/editor-setup-vscode-kiro.md](docs/editor-setup-vscode-kiro.md).
Using the tools from an AI assistant (Claude, Kiro, Continue): [docs/ai-assistant-guide.md](docs/ai-assistant-guide.md).

---

## Prerequisites

| Requirement | Needed for | Notes |
|---|---|---|
| Microsoft Edge | both modes | Run on the host, not in a container. Windows, macOS, or Linux (`microsoft-edge-stable`). |
| Microsoft account with Copilot access | both modes | Work/school (Entra) accounts use `copilot.cloud.microsoft`; personal accounts use `copilot.microsoft.com`. |
| Node.js 20+ and npm | node mode | Check with `node -v`. |
| Docker (Engine, Desktop, OrbStack or Colima), with Compose v2 | docker mode | Needed only when Node.js is not installed, or if you choose `docker`. Check with `docker compose version`. |
| An MCP client | using the tools | Kiro, VS Code (Copilot Chat), Continue, Claude Code. See [editor setup](docs/editor-setup-vscode-kiro.md). |
| git, curl | optional | git adds the commit hash to the version; curl runs the health check. |

You need Node.js **or** Docker, not both. Port `3100` (bridge), `9222` (Edge debugging) and, in Docker mode, `9223` (proxy) must be free.

## Quick Start

### 1. Start the bridge

Run one script. It asks how to run the bridge, then starts a dedicated Edge (remote debugging on `9222`) and the bridge:

```bash
scripts/run.sh            # macOS / Linux
scripts\run.bat           # Windows
```

```
How do you want to run copilot-bridge?
  1) Node.js  - build and run on this machine. No Docker, no proxy.   [available, Node.js v22.x]
  2) Docker   - bridge and CDP proxy as containers. No Node.js needed. [available]
  q) Quit
Choice [1]:
```

| Option | Needs | What runs |
|---|---|---|
| **A: Node.js** | Node.js 20+ and npm | `npm ci`, `npm run build`, then `node dist/index.js` in the terminal (Ctrl+C stops it). Talks to Edge directly. |
| **B: Docker** | Docker with Compose v2 | The `cdp-proxy` container and the `copilot-bridge` container (`docker compose up -d --build`). Keeps running in the background. |

The menu marks an option as not available when its requirement is missing, and the default is the first available one. To skip the menu, pass the choice:

| Command | Effect |
|---|---|
| `scripts/run.sh node` / `scripts\run.bat node` | Option A without asking |
| `scripts/run.sh docker` / `scripts\run.bat docker` | Option B without asking |
| `scripts/run.sh auto` | Node.js if installed, otherwise Docker, without asking. Also the behaviour when there is no terminal. |
| `scripts/run.sh stop` / `scripts\run.bat stop` | Stop the Docker containers (`copilot-bridge`, `cdp-proxy`) |

`RUN_MODE=node|docker|auto` does the same as the argument.

**Switching options.** Both options use port `3100`, so only one can run. If you pick A while the Docker bridge is running, the script offers to stop it. If you pick B while a Node.js bridge is running, it tells you to stop that one first (Ctrl+C). Use `stop` to shut Docker down.

**Docker option checks.** After starting, the script waits for `/health` and checks that the container can reach Edge through the proxy. It prints a warning with the commands to check if it cannot.

Manual start, if you prefer: `./scripts/edge-login.sh` (macOS/Linux) opens the dedicated Edge, then either `npm ci && npm run build && CDP_ENDPOINT=http://127.0.0.1:9222 node dist/index.js`, or `node scripts/cdp-proxy.mjs &` followed by `docker compose up -d --build`.

### 2. Sign in once

In the Edge window that opened (profile `~/.copilot-bridge-edge`, or `%USERPROFILE%\.copilot-bridge-edge` on Windows), sign in to Copilot. Work accounts land on `copilot.cloud.microsoft`. Within a few seconds `curl http://localhost:3100/health` reports `auth: ready` (restart the bridge if it started before Edge was up). The sign-in is stored in the dedicated profile, so later launches are already signed in. Keep that Edge window open while you use the bridge.

Open <http://localhost:3100/> to see the status page, including the running version.

### Docker runtime compatibility

In Docker mode the bridge reaches the host's Edge through `host.docker.internal:9223`, which the proxy container forwards to Edge's `127.0.0.1:9222`. The proxy port is published on host loopback only: it gives full control of the browser, so never expose it to a network.

| Runtime | Status |
|---|---|
| OrbStack (macOS) | Tested. |
| Docker Desktop (macOS/Windows), Colima | Not tested. `host.docker.internal` is expected to reach host loopback the same way. |
| Docker Engine on Linux | Not tested. `run.sh` runs the proxy on the host network and binds it to the Docker bridge gateway address, because `host.docker.internal` is not host loopback there. |
| Podman | Not supported by the scripts. |

### 3. Register in your editor

The URL is the same everywhere — only the config file format differs. Use `http://localhost:3100/mcp`. (OrbStack users may also have a `*.orb.local` domain for the container; it is optional and Mac/OrbStack-only, so don't share it in config files.)

**Kiro** — `~/.kiro/settings/mcp.json` (user) or `.kiro/settings/mcp.json` (workspace); sample: [configs/kiro-mcp-config.json](configs/kiro-mcp-config.json)

```json
{
  "mcpServers": {
    "copilot-bridge": {
      "url": "http://localhost:3100/mcp",
      "disabled": false,
      "autoApprove": ["ask_copilot", "ask_copilot_batch", "review_code", "get_status"]
    }
  }
}
```

- `mcpServers` — root key for Kiro.
- `url` — the bridge's MCP endpoint (container port 3100, path `/mcp`).
- `disabled` — `true` keeps the entry but doesn't connect.
- `autoApprove` — tools that run without a confirmation prompt.

Click **Reconnect** in Kiro's MCP panel; all four tools appear.

**VS Code** — `.vscode/mcp.json` (workspace) or `~/Library/Application Support/Code/User/mcp.json` (user); sample: [configs/vscode-mcp-config.json](configs/vscode-mcp-config.json)

```json
{
  "servers": {
    "copilot-bridge": {
      "type": "http",
      "url": "http://localhost:3100/mcp"
    }
  }
}
```

- `servers` — root key for VS Code (not `mcpServers`).
- `type: "http"` — Streamable HTTP transport.
- `url` — the same endpoint as Kiro.

Open Command Palette → **MCP: List Servers** to start/inspect it. VS Code asks for approval per tool call.

**Continue** (VS Code / JetBrains) — `.continue/mcpServers/copilot-bridge.yaml`; see [editor setup](docs/editor-setup-vscode-kiro.md#continue-vs-code--jetbrains-extension)

**Claude Code** (CLI or VS Code extension)

```bash
claude mcp add --transport http --scope user copilot-bridge http://localhost:3100/mcp
claude mcp list
```

Reload the window (or start a new session) afterwards — MCP servers load at session start. Type `/mcp` in the extension to see status.

---

## How to Use From Kiro / VS Code

Ask naturally — the editor calls the matching tool:

```
ask Copilot: "what is the difference between TCP and UDP?"
ask Copilot to review #File:src/browser.ts for security issues
ask Copilot "summarize the MCP protocol" and start a new chat
call get_status on copilot-bridge
review these two files for security issues and return findings as JSON
```

- `ask_copilot` — plain prompt; Copilot's full reply returns to the chat.
- `review_code` — send a file/snippet with an instruction; tell the editor to apply the suggested changes afterwards.
- `newChat: true` — start a fresh Copilot conversation (default continues the current one).
- `ask_copilot_batch` — several prompts in order.
- `get_status` — version, auth, Edge reachability, queue and call statistics.

---

## Authentication Details

The bridge **attaches to your signed-in Edge over CDP** (`CDP_ENDPOINT`) and types into a real Copilot tab, leaving your other tabs alone. No cookies or tokens are copied.

- Node mode connects to `http://127.0.0.1:9222`.
- Docker mode connects to `host.docker.internal:9223`; `scripts/cdp-proxy.mjs` forwards that to Edge's `127.0.0.1:9222` and rewrites the `Host` header, which Edge otherwise rejects. Set `CDP_TARGET_HOST` and `CDP_PROXY_HOST` to change where it connects and listens.
- The session check requires a visible chat input and a non-"Sign in" page, so a sign-in wall is reported as `unauthenticated` instead of `ready`.
- `COPILOT_URL` selects the entry page (`https://copilot.cloud.microsoft/` for work accounts, `https://copilot.microsoft.com/` for personal).

**Re-sign-in when:** Microsoft expires the session. Sign in again in the dedicated Edge window; no resync is needed.

If `CDP_ENDPOINT` is unset the bridge falls back to a cookie-import mode (`POST /auth/sync`, `POST /auth/cookies` with `scripts/sync-cookies.mjs`). It is not needed for normal use.

---

## MCP Tool Reference

Four tools. Every tool that sends a prompt accepts the **call options** and **output options** below. A quick usage guide for AI assistants is in [docs/ai-assistant-guide.md](docs/ai-assistant-guide.md).

**Call options** (`ask_copilot`, `ask_copilot_batch`, `review_code`)

| Parameter | Type | Default | Description |
|---|---|---|---|
| `timeoutMs` | number (5000–900000) | server `RESPONSE_TIMEOUT_MS` | Max wait for the reply |
| `retries` | number (0–3) | `0` | Extra attempts on failure; retries run in a fresh chat |
| `dryRun` | boolean | `false` | Return the exact prompt that would be sent, without contacting Copilot |

**Output options**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `maxChars` | number | — | Truncate the returned text (saves tokens; saved files stay complete) |
| `includeSources` | boolean | `false` | Append cited source URLs |
| `save` | `("md"\|"html"\|"json")[]` | — | Also write the full reply to `output/` (see [Saving replies](#saving-replies)) |

### `ask_copilot`

Send any prompt to Copilot and return its response.

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `prompt` | string | ✓ | — | The message to send |
| `newChat` | boolean | | `false` | Start a new conversation thread |
| `systemPrompt` | string | | — | Role or standing instruction placed before the prompt |
| `format` | `"markdown"\|"json"` | | `markdown` | `json` asks for a single JSON block and returns it parsed and pretty-printed |

**Returns:** Copilot's response as compact Markdown (tables, fenced code blocks with language, lists; no citation chips or UI labels), or JSON when `format` is `json`.

### `ask_copilot_batch`

Send up to 10 prompts one after another and return all answers in order. They share one conversation by default.

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `prompts` | string[] (1–10) | ✓ | — | Prompts, in order |
| `newChat` | boolean | | `false` | New thread before the first prompt |
| `newChatEach` | boolean | | `false` | New thread for every prompt (independent questions) |
| `stopOnError` | boolean | | `true` | Stop at the first failure |
| `systemPrompt` | string | | — | Placed before every prompt |

**Returns:** `### Answer N` sections. `isError` is set if any prompt failed.

### `review_code`

Send code with a specific instruction. Copilot returns analysis, explanation, or a rewritten version.

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `instruction` | string | ✓ | — | What to do — e.g. "review for bugs", "explain this", "refactor to use async/await" |
| `code` | string | one of `code`/`files` | — | Full file content or a snippet |
| `files` | `{filename?, language?, code}[]` (max 10) | one of `code`/`files` | — | Several files reviewed together |
| `language`, `filename` | string | | — | Describe `code` |
| `focus` | `("bugs"\|"security"\|"performance"\|"tests"\|"style"\|"readability")[]` | | — | What to look for |
| `minSeverity` | `"info"\|"low"\|"medium"\|"high"` | | all | Report only findings at or above this level |
| `outputFormat` | `"markdown"\|"findings-json"\|"patch"` | | `markdown` | `findings-json`: array of `{file, line, severity, title, suggestion}`; `patch`: a unified diff |
| `newChat` | boolean | | `false` | Start a new conversation thread |

**Returns:** Copilot's response as Markdown, parsed JSON, or a diff. If the requested block is missing from the reply, the raw text is returned with a note.

---

### `get_status`

Returns the bridge state as JSON. Use it to confirm auth before calling other tools.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `refresh` | boolean | `false` | Re-check the Copilot session now (waits for running prompts, then loads the page) |

**Returns:**
```json
{
  "status": "ok",
  "name": "copilot-bridge",
  "version": "0.3.2",
  "commit": "a1b2c3d",
  "uptime": 142,
  "auth": "ready",
  "authMessage": "Authenticated — cookies active",
  "browser": { "mode": "cdp", "edgeReachable": true, "edgeVersion": "Edg/…", "tabOpen": true, "tabUrl": "https://copilot.cloud.microsoft/chat" },
  "queue": { "queued": 0, "inFlight": false, "calls": 12, "errors": 1, "avgLatencyMs": 18400, "lastSuccessAt": "2026-10-25T21:40:00.000Z", "lastError": null },
  "config": { "copilotUrl": "https://copilot.cloud.microsoft/", "responseTimeoutMs": 240000, "selectorTimeoutMs": 30000 },
  "node": "22.x"
}
```

`auth` values: `unauthenticated` | `syncing` | `ready` | `error`. The counters reset when the bridge restarts. `edgeReachable` is `null` outside CDP mode.

---

## Saving replies

Pass `save` to keep the full reply as a file. Files are written to `./output/` on the host (bind mount; git-ignored) and the paths are appended to the tool result. Ask naturally: *"ask Copilot …, save as md and json"*.

| Format | Contents |
|---|---|
| `md` | Title (the prompt), timestamp, reply as Markdown, sources |
| `html` | Standalone styled page (light/dark) with the prompt, rendered reply, real `<table>`s and `<pre><code class="language-…">` blocks, sources |
| `json` | `{ prompt, createdAt, markdown, html, codeBlocks[], sources[] }` — each code block is `{ language, code }`, plus `parsed` when it is valid JSON, so other data formats and JSON can be consumed programmatically |

Files are named `YYYYMMDD-HHMMSS-<prompt-slug>.<ext>`. Set `OUTPUT_DIR` / `HOST_OUTPUT_DIR` to change locations.

**Reducing token use:** the returned text is already stripped of citations and UI noise. Use `maxChars` to cap it, and `save` to keep the complete answer on disk so the model only reads what it needs.

---

## Example: splitting work between Copilot and Claude

[examples/thumbnail-lambda](examples/thumbnail-lambda/README.md) is an AWS Lambda + API Gateway (API key) thumbnail service where **Copilot wrote the Terraform** through this bridge and **Claude wrote the Python app and tests**. Measured on that run: ~40 % fewer Claude output tokens than writing everything in Claude, **but only** when the reply is saved (`save: ["json"]`, small `maxChars`) and turned into files with `scripts/extract-code.mjs`. Reading the reply and re-typing it was ~13 % *more* expensive. Details and caveats are in the example's README.

```bash
node scripts/extract-code.mjs output/<reply>.json <outDir>   # blocks starting with "# file: name" → files
```

---

## HTTP Endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/` | Status page with auth state |
| `GET` | `/health` | JSON health probe (used by Docker); includes the version |
| `GET` | `/version` | `{ name, version, commit }` |
| `GET` | `/debug/dom` | Inputs found on the live Copilot page (selector debugging) |
| `POST` | `/auth/recheck` | Re-check the Copilot session (button on the status page) |
| `POST` | `/auth/sync` | Cookie mode only: copy cookies from host Edge profile |
| `POST` | `/auth/cookies` | Cookie mode only (rejected in CDP mode): import `{ cookies: [...] }` (see `scripts/sync-cookies.mjs`) |
| `ALL` | `/mcp` | MCP Streamable HTTP transport |

---

## Environment Variables

| Variable | Default | Description |
|---|---|---|
| `PORT` | `3100` | HTTP port |
| `ALLOWED_HOSTS` | `localhost,127.0.0.1,[::1]` | Hostnames accepted in the `Host` header (DNS-rebinding protection); other hosts get `403`. Add a name here if you reach the bridge through one |
| `PROFILE_DIR` | `/app/profile` | Container Chromium profile directory |
| `HOST_PROFILE_DIR` | `/host-profile` | Mount point for the host Edge profile |
| `CDP_ENDPOINT` | `http://host.docker.internal:9223` (compose), `http://127.0.0.1:9222` (node mode) | Drive the signed-in Edge; unset for cookie-import mode |
| `COPILOT_URL` | `https://copilot.microsoft.com/` (compose: `copilot.cloud.microsoft`) | Copilot entry page |
| `OUTPUT_DIR` | `/app/output` | Where `save` writes inside the container |
| `HOST_OUTPUT_DIR` | _(none)_ | Host path shown in "Saved:" results (set in compose) |
| `GIT_SHA` | `dev` | Build commit shown next to the version (set by the run scripts and the Docker build) |
| `RUN_MODE` | _(ask)_ | Choice for `scripts/run.sh` / `run.bat` without the menu: `node`, `docker`, `auto`, `stop` |
| `LOG_LEVEL` | `info` | `debug` / `info` / `error` |
| `RESPONSE_TIMEOUT_MS` | `120000` | Max wait for Copilot to finish responding |
| `SELECTOR_TIMEOUT_MS` | `30000` | Max wait for DOM elements |

---

## Project Structure

```
copilot-bridge-mcp/
├── src/
│   ├── index.ts        — HTTP server, status page, /version, MCP transport wiring
│   ├── mcp-server.ts   — Tool definitions (ask_copilot, ask_copilot_batch, review_code, get_status)
│   ├── prompts.ts      — Prompt building for review_code, JSON / diff output instructions
│   ├── browser.ts      — Playwright automation, auth state machine
│   ├── reply.ts        — Reply → Markdown/HTML/code blocks/sources; saving to output/
│   ├── cookies.ts      — Cookie-import mode helpers
│   ├── config.ts       — All config from environment variables
│   ├── version.ts      — Version (package.json) and build commit (GIT_SHA)
│   └── logger.ts       — Structured logger
├── dist/               — Compiled JS (after npm run build)
├── Dockerfile          — Multi-stage: TS builder + Playwright/Chromium runtime
├── docker-compose.yml  — Docker Compose config
├── scripts/
│   ├── run.sh           — start Edge + bridge on macOS/Linux; menu: Node.js or Docker; `stop`
│   ├── run.bat          — start Edge + bridge on Windows; menu: Node.js or Docker; `stop`
│   ├── edge-login.sh    — launch dedicated Edge with remote debugging (macOS/Linux)
│   ├── cdp-proxy.mjs    — proxy 9223 → 9222 (Host header rewrite) for Docker mode
│   ├── extract-code.mjs — saved reply (json) → files, no LLM involved
│   └── sync-cookies.mjs — cookie-import mode helper
├── examples/thumbnail-lambda/ — Copilot (Terraform) + Claude (Python) example with token measurements
├── output/             — Saved replies (git-ignored)
├── docs/               — architecture and editor setup (VS Code / Kiro / Claude Code), security fix plan, AI assistant guide
├── CHANGELOG.md        — release notes
└── configs/            — sample MCP configs (kiro-mcp-config.json, vscode-mcp-config.json)
```

---

## Versioning

The release version is the `version` field in `package.json` ([Semantic Versioning](https://semver.org/)); release notes are in [CHANGELOG.md](CHANGELOG.md). It is shown on the status page (<http://localhost:3100/>), at `GET /version`, in `/health`, in `get_status` and in the MCP server info. The commit hash appears next to it when `GIT_SHA` is set, which the run scripts and `docker compose build` do automatically.

To release: update `CHANGELOG.md`, run `npm version <patch|minor|major>` (this edits `package.json` and `package-lock.json`), rebuild the container, and tag the commit.

---

## Development

```bash
npm install           # install deps
npm run build         # compile TypeScript → dist/
npm run dev           # run locally with tsx (no Docker)
npm run typecheck     # type-check only, no output
```

Rebuild and restart the container after source changes:
```bash
docker compose up -d --build
```

---

## Troubleshooting

**Editor shows the server as disconnected / no tools**
The container isn't running or isn't on port 3100. Run `docker compose up -d` and `curl http://localhost:3100/health`.

**`auth: unauthenticated`**
Make sure the dedicated Edge (started by `scripts/run.sh` / `run.bat`) is open and signed in to Copilot and, in Docker mode, the `cdp-proxy` container is running (`docker ps`). Then restart the bridge (`docker compose restart` in Docker mode). The status page at http://localhost:3100 shows the reason.

**`SingletonLock` / "profile in use by another process"**
The bridge clears all Chromium lock files before each launch and passes `--no-process-singleton-locks`. If you still see this, run:
```bash
docker compose down -v && docker compose up -d
```
This wipes the volume (removing stale locks). Only relevant to the legacy cookie mode; in the default CDP mode the bridge drives your Edge and uses no container profile.

**Copilot chat input not found (selector timeout)**
Copilot's DOM changes occasionally. The selectors are in the `SEL` object at the top of `src/browser.ts`. Update them if Copilot redesigns the chat input.

**Tool call returns empty response**
The stable-text polling fallback may have timed out. Increase `RESPONSE_TIMEOUT_MS` in `docker-compose.yml` for long Copilot responses.

---

## Known Limitations

- Relies on DOM scraping — not an official Microsoft API. Selectors may break when Copilot updates its UI.
- Conversation history lives in the browser tab. It resets when the container restarts or when `newChat: true` is passed.
- Personal use only. Microsoft's Terms of Service apply.