# @napster-corp/edge-mcp

> Let an AI agent actually operate your web app — by exposing the app's real operations as tools the agent can call, on top of the WebMCP standard.

[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![standard: WebMCP](https://img.shields.io/badge/standard-WebMCP-purple.svg)](https://github.com/webmachinelearning/webmcp)

Most AI agents on websites today are observers. A voice-and-video assistant on
your homepage, or a chatbot in the corner, can answer questions about your
product, read off pricing, maybe file a support ticket. But when the user says
"add the 65-inch OLED to my cart and check out," the agent's answer is
something like "sure, click the Add to Cart button." It can't actually do it.
The user still drives the UI by hand.

WebMCP closes that gap. Your app declares which of its real operations an
agent is allowed to run — `products.search`, `cart.add`,
`checkout.placeOrder`, whatever you choose to expose — and any compatible
agent running in the same page can call them. The agent doesn't simulate your
app; it operates the app, using the same functions your buttons and forms
already call. The cart drawer slides open. The product page navigates. The
order gets placed.

This is **the Model Context Protocol, but for the browser.** The original MCP
exposes server-side tools to an AI client over a wire protocol; WebMCP exposes
the app's in-browser tools to whichever agent is running on the same page,
through `document.modelContext`. A compatible agent SDK finds the app's tools
at runtime and wires itself up — no glue code, nothing to dispatch by hand.

The **Napster Edge MCP** is a thin layer on top of that standard. It
**polyfills** WebMCP so `document.modelContext` exists in every browser
(including carrying tool `annotations` through `getTools()`), and it **adds
value** on top — live-state resources and rich console debugging — without changing
how you register tools. Your `registerTool` code is standard WebMCP: remove the toolkit
and it still works against the native browser API. Safety levels are expressed
with the standard annotation hints, not a proprietary tier.

```bash
npm install @napster-corp/edge-mcp
```

No npm or build step in your site? There's a [pre-bundled script-tag build](#no-build-step-the-script-tag-build) too.

---

## Why this exists

Even when teams try to make a website agent that *does* things instead of
just talking, the usual path is to build a second, parallel system next to
the app: copy your data into a vector store, hand-wrap your APIs as agent
tools, write a workflow mapping layer, and keep all of it in sync with the
real application every time it ships. The parallel system drifts. The agent
makes confident, wrong claims based on stale knowledge — and when it does
try to act, it acts on a copy of the app, not the app itself.

WebMCP inverts this. Instead of duplicating your app for the agent, you
point the agent **at** the live app. You expose an approved set of operations
the agent is allowed to invoke and an approved set of state slices the agent
is allowed to observe. The agent calls the same functions the UI calls; the
app stays the source of truth.

Two consequences worth stating up front:

- **The UI stays in sync automatically.** An agent's `cart.add` updates the
  one cart store, and every component bound to it re-renders. You don't write
  sync code.
- **The agent runs as the signed-in user.** The toolkit doesn't grant new
  permissions; it picks a subset of the user's existing rights and makes them
  callable through the agent. Auth, validation, and authorization stay
  exactly where they already are.

And one that's specific to building on a standard:

- **Zero lock-in.** Tools are registered through the native
  `document.modelContext.registerTool` API. If the customer removes this
  toolkit, their tool code keeps working against the browser's own WebMCP
  implementation — and any standards-compliant WebMCP agent can already drive
  them. The toolkit's extras (resources, console debugging) layer on top
  without owning the call site.

---

## What importing the package does

Importing `@napster-corp/edge-mcp` is a **browser-only side effect**. On
import it does two things:

1. **Polyfills WebMCP.** It initializes the vendored WebMCP polyfill — a fork of
   [`@mcp-b/webmcp-polyfill`](https://www.npmjs.com/package/@mcp-b/webmcp-polyfill)
   whose source lives in `src/webmcp-polyfill/` (**not** a runtime dependency),
   patched to surface tool `annotations` through `getTools()` — so
   `document.modelContext` exists cross-browser. The polyfill installs
   **unconditionally**: a pre-existing `document.modelContext` — a native
   browser implementation or a foreign polyfill — is **replaced**, because
   current native surfaces drop tool `annotations` from `getTools()` and reject
   foreign tool objects in `executeTool()` (the replaced implementation is
   retained internally, see `getReplacedModelContext()`). Only a prior install
   of this same fork is left in place, so double imports and HMR are safe.
2. **Installs the resource extension.** It adds an MCP-shaped live-state
   "resource extension" onto `document.modelContext` (see
   [Live state](#live-state--the-eyes)).

Outside a browser — SSR (Next.js / Nuxt / Remix / SvelteKit), web workers, edge
runtimes — importing the package **does nothing and touches no globals**. The
real surface comes up in the browser when the bundle hydrates. You don't need
to add any guards in your own code; the package handles it.

```ts
// src/edge-mcp/index.ts
import '@napster-corp/edge-mcp';   // polyfill + resource extension (side effect)
import './tools';                         // tools/ — one descriptor file per tool; tools/index.ts registers them
import './resources';                     // your registerResource calls
```

---

## No build step? The script-tag build

Not every website has npm and a bundler — server-rendered templates (Rails,
Django, PHP), static HTML sites. For those, the package ships
a pre-bundled, minified browser build (`dist/edge-mcp.iife.min.js`, all
dependencies inlined, ~38 KB before gzip). Because it's published inside the
npm package, any npm CDN serves it — no separate CDN infrastructure:

```html
<script src="https://cdn.jsdelivr.net/npm/@napster-corp/edge-mcp@0.1/dist/edge-mcp.iife.min.js"></script>
```

Loading the script has the **identical side effect** as importing the package:
it polyfills `document.modelContext` and installs the resource extension. From
there, everything is the standard surface — plain inline scripts, zero imports:

```html
<script>
  document.modelContext.registerTool({
    name: 'contact.submit',
    description: 'Submit the contact form.',
    inputSchema: { type: 'object', properties: { email: { type: 'string' } }, required: ['email'] },
    annotations: { destructiveHint: true },
    async execute({ email }) {
      // calls the site's real code, same as the form's own submit handler
      return submitContactForm(email);
    },
  });
</script>
```

Details:

- **Pin the version** in the URL (`@0.1`, not `@latest`) so your site doesn't
  silently pick up future majors.
- The module's helper exports are available on a **`window.EdgeMCP`** global
  (`EdgeMCP.registerResource`, `EdgeMCP.getModelContext`, `EdgeMCP.setDebug`, …).
  You rarely need them — `registerResource` is also installed directly on
  `document.modelContext`.
- Load the script (and any registrations) **before** your agent SDK initializes,
  so the tools are registered by the time the agent reads `document.modelContext`.
- `unpkg.com/@napster-corp/edge-mcp@0.1/dist/edge-mcp.iife.min.js` works the
  same way if you prefer unpkg over jsDelivr; self-hosting the file is fine too.

---

## Quick example

The website developer writes **standard WebMCP tools** — there's no
Napster-specific call required to register a tool:

```ts
// src/edge-mcp/tools/cart-add.ts — one tool per file in the recommended layout
import type { ToolDescriptor } from '@napster-corp/edge-mcp';

export const tool: ToolDescriptor = {
  name: 'cart.add',
  description: 'Add a product to the cart.',
  inputSchema: {
    type: 'object',
    properties: { productId: { type: 'string' } },
    required: ['productId'],
  },
  // Safety rides on the STANDARD annotation hints. A reversible write sets
  // neither readOnlyHint nor destructiveHint. (read → readOnlyHint: true;
  // needs-confirmation → destructiveHint: true.) See "Safety annotations" below.
  annotations: { readOnlyHint: false },
  async execute({ productId }) {
    cartStore.add(productId as string);
    return { content: [{ type: 'text', text: 'Added to cart' }] };   // standard MCP result shape
  },
};

// src/edge-mcp/tools/index.ts registers it on the standard surface:
//   import { tool as cartAdd } from './cart-add';
//   document.modelContext.registerTool(cartAdd);
```

That's the entire integration for a tool — pure standard WebMCP, no
Napster-specific registration call. A WebMCP-aware agent SDK
detects `document.modelContext`, reads the tool list, and wires itself up — no
glue code, nothing to dispatch by hand.

> `cartStore` is a stand-in for your app's own code — the same functions your
> UI's buttons and forms already call. The toolkit doesn't replace any of
> them; it exposes the ones you choose to a connected agent.

See [`examples/app-side.ts`](./examples/app-side.ts) for the full app-side
pattern.

### Safety, and two value-adds

Safety is **not** a Napster invention — it rides on the standard annotation
hints, so it stays portable. The two genuine add-ons live **off** the standard
call site:

| Concern | How | Standard? |
| --- | --- | --- |
| Safety / confirmation | `registerTool({ ..., annotations: { readOnlyHint, destructiveHint, idempotentHint } })` | ✅ standard MCP annotation hints |
| Live state | `registerResource({ uri, get, subscribe })` | ✗ WebMCP hasn't formalized resources yet; consumed over Napster's own path |

---

## Have a coding agent set it up for you

If you'd rather not work through tool and state design by hand, the public
**Napster Omniagent skills** hub ships composing skills that any Agent
Skills-compatible tool (Claude Code, Cursor, Codex, OpenCode, etc.) can load.

```bash
npx skills add Napster/omniagent-api-skills
```

The skills that set up this toolkit (alongside the rest of the Omniagent API
skills in that hub):

- **`edge-mcp-setup`** — the main entry point and orchestrator. Runs the
  skills below in order and walks the developer through sign-off.
- **`edge-mcp-plan`** — invoked by `edge-mcp-setup` as its first
  step. Analyzes the codebase, proposes a curated starter plan (tools, live-state
  resources, and deliberate withholds), walks the developer through it until
  approved. No file output — the plan lives in the conversation, the code is the
  record.
- **`edge-mcp-implement`** — turns the approved plan into code: installs the
  package, registers the agreed tools and resources against the app's real
  code, and verifies them at runtime.

In your project, just say what you want in plain language — "set up WebMCP",
"agentify this app", "what should I expose to the agent?", "add a panel for
testing" — and the matching skill fires.

These skills stop once your WebMCP surface is built. Connecting an actual agent
(Napster's Omniagent, or any other WebMCP-compatible vendor) is a separate step
handled by that vendor's own SDK or skills. The same `Napster/omniagent-api-skills`
hub covers the Napster Omniagent end (personas, agents, deploy channels); its Web
SDK auto-attaches to `document.modelContext` at runtime.

---

## Core concepts

### Tools — the hands

What the agent can do. One tool per real operation you choose to expose. Name
them in your app's own domain terms (`products.search`, `cart.add`,
`orders.cancel`), not in agent-product terms.

`execute` calls your app's **real** code — the same function the UI's own
button or form submits to. Composing several real operations into one tool is
fine (and often necessary). What's forbidden is re-deriving business logic the
app already owns; if you find yourself recomputing a price the app already
calculates, stop.

Register with the standard `document.modelContext.registerTool(...)`. Express
how carefully the agent should commit a tool with the standard `annotations`
hints (see [Safety annotations](#safety-annotations) below).

### Live state — the eyes

What the agent can perceive that it didn't get back from a tool. Resources are
the **exception, not the rule**. Most things don't need one — if a tool returns
its result, the agent already knows.

Add a resource only for state that changes **out of band**:

- The user edits something by hand (cart quantity, filter, navigation)
- The server changes state over time (an order moves from `processing` to
  `shipped` mid-conversation)

Pure pull state — something you could just read on demand — is better modeled
as a read-only tool than as a resource. And if a tool already returns the
answer, do **not** add a resource that mirrors it. A search that returns its
results inline needs no `searchResults` resource; the agent has the data
already.

### Safety annotations

Express how carefully a tool should be committed with the **standard** WebMCP /
MCP annotation hints — no proprietary field. Consumers (the Web SDK / agent)
read these off `getTools()` to gate confirmation flows.

| Level | `annotations` | Example | Confirmation |
| --- | --- | --- | --- |
| read | `{ readOnlyHint: true }` | search, look up, compare | None — call freely |
| reversible | `{}` (neither hint) | add to cart, save draft, apply filter | Brief announce |
| needs confirmation | `{ destructiveHint: true }` | place order, cancel, send | Explicit user confirmation, wait for consent |

Set the safer level when in doubt. **The consumer enforces confirmation — never
the model.** A tool with no annotations is treated as reversible.

By convention here, **`destructiveHint: true` means "confirm with the user
first."** MCP defines `destructiveHint` as a non-additive/destructive update,
which is slightly narrower — so also set it for additive-but-final actions
(submit, send, place order) where you still want a confirmation gate.

Set `idempotentHint: true` when the underlying operation tolerates safe retries
(e.g. via an idempotency key on the server), and `untrustedContentHint: true`
when the tool returns content that may carry injected instructions.

---

## API

### Registering a tool (standard)

There is **no** Napster registration wrapper. Register on the standard surface
and express safety with the standard `annotations` hints. The polyfill (vendored
here) surfaces those annotations through `getTools()`, so any consumer — the
Napster Web SDK or any WebMCP agent — reads them the standard way.

```ts
const unregister = document.modelContext.registerTool({
  name: 'checkout.placeOrder',
  description: 'Submit the cart for purchase.',
  inputSchema: {
    type: 'object',
    properties: {
      paymentMethodId: { type: 'string' },
      addressId: { type: 'string' },
    },
    required: ['paymentMethodId', 'addressId'],
  },
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,    // ⇒ consumer confirms with the user before calling
    idempotentHint: true,     // safe to retry
    // untrustedContentHint: true   // if output may carry injected instructions
  },
  async execute({ paymentMethodId, addressId }) {
    const order = await placeOrder(paymentMethodId, addressId);
    return { content: [{ type: 'text', text: `Order ${order.id} placed` }] };
  },
});

// pass { signal } to registerTool and abort it to unregister.
```

**Standard annotation hints** (all optional, read off `getTools()` by the consumer):

| Hint | Meaning |
| --- | --- |
| `readOnlyHint` | the tool only reads state — call freely |
| `destructiveHint` | **by our convention: confirm with the user before calling** |
| `idempotentHint` | safe to retry with the same args |
| `untrustedContentHint` | output may carry untrusted / injected content |

A tool with no annotations is treated as reversible (announce-then-run).

### Reading tools back — `getTools()` / `executeTool()` (consumer side)

If you build anything that *consumes* the surface — an agent bridge, an inspector — two wire-contract details will bite you if you don't
know them:

**1. `getTools()` returns each tool's `inputSchema` as a JSON *string*, not an
object.** That is Chromium's native `getTools()` contract, and the polyfill
matches it for interoperability (you *register* an object; you *read back* a
string). Parse before use:

```ts
const tools = await document.modelContext.getTools();
const schema = JSON.parse(tools[0].inputSchema ?? '{"type":"object"}');
```

**2. `executeTool(tool, argsJson)` resolves with a JSON *string* of the standard
result envelope** — `JSON.stringify({ content: [{ type: 'text', text: '…' }] })`.
Unwrap it to render the output (and note the `text` may itself be JSON if the
tool stringified data into it):

```ts
const raw = await document.modelContext.executeTool(tool, JSON.stringify({ query: 'laptop' }));
const text = raw === null ? null : JSON.parse(raw).content?.[0]?.text;
```

It resolves with `null` when the tool's `execute` returned `undefined`, and
rejects on an unknown tool, argument-validation failure, or an aborted
`options.signal`. Portability note: the polyfill resolves the tool by `name`,
but Chromium's native implementation requires the exact object `getTools()`
returned — always pass the `getTools()` handle, never a hand-built object.

**TypeScript:** the package ships an ambient declaration for
`document.modelContext` (typed with the resource extension included), so
`document.modelContext.registerTool(...)` and `.registerResource(...)`
type-check as soon as `@napster-corp/edge-mcp` is imported anywhere in the
compilation. Delete any hand-written `webmcp.d.ts` — a local re-declaration of
`Document.modelContext` will now conflict.

### `registerResource(resource)`

Register a live-state resource, modeled on MCP resources. Use it only for
[out-of-band state](#live-state--the-eyes).

```ts
import { registerResource } from '@napster-corp/edge-mcp';

registerResource({
  uri: 'state://cart',
  name: 'cart',
  description: 'The current shopping cart',   // optional
  mimeType: 'application/json',               // optional
  get: () => cartStore.getCurrent(),
  subscribe: (onChange) => cartStore.subscribe(onChange),   // optional; return an unsubscribe fn
});
```

The consumer-side surface lives on `document.modelContext` (installed by the
import side effect):

| Member | Purpose |
| --- | --- |
| `getResources()` | List registered resources |
| `readResource(uri)` | Read the current value of one resource |
| `subscribeResource(uri, handler)` | Subscribe to changes for one resource |
| `resourceupdated` event | `CustomEvent` with `detail = { uri, value }` |
| `resourcelistchanged` event | Fired when the resource list changes |

> This resource channel is consumed by the Napster agent over its own path. It
> is **not interoperable** with third-party WebMCP agents until the standard
> formalizes resources. The standard tool surface, by contrast, is fully
> interoperable today.

### Inspecting your surface — DevTools and console logs

There is no bundled inspector UI. Two built-in ways to see what's going on:

**Chrome DevTools → WebMCP panel.** Because registered tools are mirrored into
the browser's native registry (see the native-registry integration in the
changelog), Chrome's DevTools WebMCP panel lists every tool with an invocation
counter, logs each call with its status/input/output, and can invoke tools
manually — no toolkit code required. Opt out with
`{ mirrorToNativeRegistry: false }` if you need execution to stay fully local.

The panel is not on by default — WebMCP is an origin-trial feature (Chrome
149+). For local development, enable two flags and relaunch:
`chrome://flags/#enable-webmcp-testing` (**WebMCP for testing**) and **WebMCP
support in DevTools**. The panel then appears under DevTools → **Application**
→ **WebMCP**. The flags matter only for the panel: the toolkit works in every
browser without them (the polyfill provides `document.modelContext` regardless;
the native mirror stays dormant until a native surface exists).

**Console flow logs.** Turn on debug logging and the toolkit narrates the whole
surface with a colored `[edge-mcp]` prefix:

```ts
import { setDebug } from '@napster-corp/edge-mcp';
setDebug(true);
// or, at runtime from the browser console, no rebuild needed:
globalThis.__EDGE_MCP_DEBUG__ = true;
```

What you'll see, per event:

| Log line | When |
| --- | --- |
| `registered tool "…"` / `unregistered tool "…"` | tool lifecycle |
| `tool "…" called (locally \| via native registry) — args:` | every invocation, with arguments |
| `tool "…" responded:` / `tool "…" failed:` | every result or error |
| `registered resource "…"` | resource lifecycle |
| `resource "…" changed →` / `read →` | every push and every consumer read |
| `resource "…" — consumer subscribed / unsubscribed` | who is watching |

To hand-test a tool without an agent, call it from the DevTools console — this
is also the snippet the Napster skills ask developers to paste and report back:

```js
const tools = await document.modelContext.getTools();
const t = tools.find(x => x.name === 'cart.add');
await document.modelContext.executeTool(t, JSON.stringify({ productId: 'sku_123' }));
```

### Adapter exports

The single swap-point over the polyfill, for code that wants the model-context
object directly instead of reaching for the global:

| Export | Returns |
| --- | --- |
| `getModelContext()` | `document.modelContext` (with a deprecated `navigator.modelContext` fallback) |
| `getModelContextWithResources()` | the same object, with the resource extension surface |
| `isBrowserEnvironment()` | `true` only in a real browser |

---

## How an agent discovers your app

A WebMCP agent — or the Napster Companion Web SDK — attaches with no
coordination from you:

1. **Detect** by the presence of `document.modelContext`.
2. **Read tools** via `getTools()`.
3. **Invoke** via `executeTool(toolInfo, JSON.stringify(args))`.
4. **Observe tool changes** via the `toolchange` event — a bare `Event` with no
   `detail`; re-read `getTools()` when it fires.
5. **Relay live state** via `subscribeResource` / the `resourceupdated` event.

Because every one of those steps is standard, **the agent attaches to any
WebMCP-enabled site** — even one that never installed this toolkit. Safety
annotations travel on the standard tools, so they work anywhere; the Napster
live-state resource extension simply lights up when the toolkit is present and
is absent otherwise; nothing breaks either way.

---

## Hosting

The toolkit is browser-only and runs in the same module graph as your UI. A few
notes worth knowing up front:

- **Recommended file layout.** Keep your WebMCP wiring in a `src/edge-mcp/`
  folder: `index.ts` imports the toolkit and wires everything up, a `tools/`
  folder holds one descriptor file per tool with `tools/index.ts` as the
  registrar that `document.modelContext.registerTool`s them all, and
  `resources.ts` holds your `registerResource` calls. (A `handles.ts` joins them
  only if a tool's `execute` needs framework context like a router hook.)
- **Imperative actions from outside the component tree.** A tool's `execute`
  lives in a plain module. Things like navigation often only exist inside the
  framework (e.g. React Router's `useNavigate` hook). Register a module-level
  handle from an in-tree component at mount and have `execute` call through
  that; otherwise it has no way to drive navigation.
- **Server-side rendering is a no-op.** Outside the browser (SSR, workers, edge
  runtimes), importing the package does nothing and touches no globals. This is
  deliberate: the toolkit connects an in-browser agent to in-browser state;
  running it on the server would bleed per-user state through the Node
  process's shared globals. The real surface comes up in the browser when the
  bundle hydrates.

---

## Automation — keep your tools folder in sync

Your tool list is hand-curated, but it drifts as the app changes — a route gets
renamed, an operation is removed, a new one becomes worth exposing. The package
ships an **opt-in agent** that re-analyzes the app and reconciles your tools
folder (e.g. `src/edge-mcp/tools/` — adding, updating, and removing one
descriptor file per tool and keeping `tools/index.ts` in sync) to match the
current code — runnable from a post-commit hook locally, or from CI on a pull
request. It produces **uncommitted** changes you review.

It uses the `edge-mcp-plan` / `edge-mcp-implement` skills from the
public `Napster/omniagent-api-skills` hub as the methodology (fetched at run
time), and by default runs on the **Claude Agent SDK**
(`@anthropic-ai/claude-agent-sdk`, Claude Opus 4.8).

### Setup (in the host app)

```bash
npm install @napster-corp/edge-mcp
npm install -D @anthropic-ai/claude-agent-sdk   # the default engine's runtime
npx edge-mcp install-hook                        # the marker-gated [edge-mcp] post-commit hook
export ANTHROPIC_API_KEY=...                      # local: your key; CI: a secret
```

Put your engine's key where the CLI can read it — exported, in CI as a secret,
or in the app's gitignored `.env.local`.

The **default** way to keep the surface in sync needs no automation at all: the
agent that changes your app reconciles the tools — and flags resource changes —
as part of the same work (see the `edge-mcp-setup` / `edge-mcp-sync` skills, which
also drop a keep-in-sync note into your repo's `CLAUDE.md` / `AGENTS.md`). The CLI
below is the **opt-in automation**, for changes that land *outside* an agent
(hand commits, teammates, CI).

### Opt-in marker (plain git hook) — `install-hook`

For commits made outside Claude Code, the post-commit hook runs **only** when a
commit message contains the marker `[edge-mcp]`:

```bash
git commit -m "feat(cart): add bulk remove  [edge-mcp]"
```

Every other commit is untouched — no agent run, no token cost. When the marker
is present, the agent analyzes the app and leaves **uncommitted** changes to
your tools folder — a post-commit hook can't amend the commit, so you review the
diff and commit it separately:

```
✎ src/edge-mcp/tools/ (unstaged)
  + cart-bulk-remove.ts → cart.bulkRemove (reversible) — src/store/cart.ts:bulkRemove
  ~ checkout-place-order.ts → checkout.placeOrder (signature changed) — src/api/checkout.ts:placeOrder
  + tools/index.ts (registrar updated)
→ review & commit when ready
```

### Run it manually / in CI

`edge-mcp generate` is the same command the hook calls — run it anywhere:

```bash
npx edge-mcp generate          # regenerate now, against the current working tree
```

On CI (e.g. a GitHub Action on `pull_request`), set the engine's key as a
secret and run `npx edge-mcp generate`, then open/update a PR with the result.
It auto-detects the tools folder (`src/edge-mcp/tools/`, `lib/edge-mcp/tools/`, …);
override with `--file path` if needed.

### What the agent may touch

Locked down by design: the agent is restricted to `Read` / `Grep` / `Glob` /
`Edit` / `Write`, with no shell — it reads the app to find real operations and
edits **only** files in your tools folder (the per-tool descriptors and
`tools/index.ts`). It does **not** edit live-state resources (`resources.ts`) —
those are subtler to get right unattended, so it flags them for a human/in-chat
pass instead. It cannot run commands, install packages, or touch git.

### Engines

Pluggable — pick with `--engine` or `EDGE_MCP_ENGINE`:

| Engine | Default | Needs | Notes |
| --- | --- | --- | --- |
| `anthropic` | ✅ | `ANTHROPIC_API_KEY` + `@anthropic-ai/claude-agent-sdk` | Claude Agent SDK (Opus 4.8); restricted to read/edit tools (no shell) |
| `copilot` | | the GitHub **Copilot CLI** + a Copilot subscription | Shells out to the CLI; no Anthropic key |

```bash
EDGE_MCP_ENGINE=copilot npx edge-mcp generate     # or: npx edge-mcp generate --engine copilot
```

The skills, prompt, path detection, opt-in marker, and uncommitted-output
policy are identical across engines — only the agent runtime changes.

> The Copilot CLI's non-interactive flags evolve, so the exact invocation is
> env-overridable rather than hardcoded — set `EDGE_MCP_COPILOT_BIN` and
> `EDGE_MCP_COPILOT_ARGS` to match your installed `copilot --help`.

### Commands

| Command | What it does |
| --- | --- |
| `edge-mcp install-hook` | Install/refresh the opt-in `[edge-mcp]` post-commit hook (idempotent; composes with an existing hook) |
| `edge-mcp generate [--engine anthropic\|copilot]` | Analyze the app and reconcile your tools folder — one file per tool + registrar (local or CI) |

---

## License

[MIT](./LICENSE).
