Skip to main content
Glama
README.md
<h1 align="center">🌱 graft</h1>
<p align="center"><b>Graft code onto any website.</b><br>Userscripts with a nervous system: hot reload + an MCP server so your coding agent can see and change live pages.<br>No build. No upload. No repack.</p>

---

## Why

Tampermonkey is great until you want your agent to help. graft keeps the userscript model (plain files, `@match` headers) and adds:

| | Tampermonkey | graft |
|---|---|---|
| Edit loop | edit in a browser box | edit a file in your editor β†’ page updates instantly |
| Coding agent can drive it | no | yes, via MCP (12 tools) |
| Agent can see the page | no | DOM outline, screenshot, console, network |
| Cross-origin fetch | `GM_xmlhttpRequest` | `graft.fetch()` |
| Intercept page fetch/XHR | no | `graft.net.onRequest / onResponse` |
| Strict-CSP sites | often blocked | persistent scripts ignore CSP |
| Old `GM_*` scripts | yes | yes, unchanged |
| Rebuild to change code | no | no |

## Install (2 minutes)

```bash
git clone https://github.com/shahaklt/graft && cd graft && npm install
node bin/graft.mjs path        # prints the extension folder
```

1. Open `chrome://extensions`, enable **Developer mode**, **Load unpacked** β†’ the `extension/` folder. *(One time. You never rebuild or repack after this.)*
2. On graft's extension card, switch on **Allow User Scripts** (lets persistent scripts ignore page CSP; without it graft falls back to eval).
3. Add the MCP server to your agent:

```bash
# Claude Code
claude mcp add graft -- node /absolute/path/to/graft/bin/graft.mjs mcp
# or straight from GitHub
claude mcp add graft -- npx -y github:shahaklt/graft mcp
```

Cursor / Claude Desktop (`mcpServers` JSON):
```json
{ "mcpServers": { "graft": { "command": "npx", "args": ["-y", "github:shahaklt/graft", "mcp"] } } }
```
Codex: `codex mcp add graft -- npx -y github:shahaklt/graft mcp`

4. Try it. Open <http://127.0.0.1:7777/playground> (served by the hub once running) and ask your agent: *"outline the playground page, add a todo, then make the heading green and keep it as a script."*

No agent? Run just the hub and hand-write scripts: `node bin/graft.mjs dev ./my-scripts`.

## Writing scripts

`~/.graft/scripts/hello.graft.js` (or any folder via `graft dev <dir>`):

```js
// ==Graft==
// @name   Hello
// @match  https://*.example.com/*
// @run-at document-idle
// ==/Graft==
graft.css(`body{outline:3px solid #9fe870}`);
graft.on('a.buy', el => el.textContent = 'buy (graft)');   // current + future elements
graft.toast('hello 🌱');
```

Save the file β†’ every open matching tab re-runs it. `graft.css`, `graft.on`, `graft.toast`, `graft.panel` and `graft.net.*` handlers are auto-cleaned on hot reload; register your own with `graft.cleanup(fn)`.

### The `graft` object (in scope in every script, never on `window`)

| API | |
|---|---|
| `graft.$ / $$` | querySelector helpers |
| `graft.css(text)` | inject style, returns remover |
| `graft.waitFor(sel, {timeout})` | promise for an element |
| `graft.on(sel, cb)` | run for existing + future matches |
| `graft.el(tag, props, ...kids)` | build DOM |
| `graft.toast(msg)` / `graft.panel({title, html})` | shadow-DOM UI, immune to page CSS |
| `graft.fetch(url, init)` | cross-origin request via the extension (`.ok .status .text() .json()`) |
| `graft.net.onRequest(fn)` / `onResponse(fn)` / `log()` | watch & rewrite page fetch/XHR |
| `graft.store.get/set/del/keys` | persistent per-script storage |
| `graft.notify(title, msg)` | desktop notification |
| `graft.cleanup(fn)` | run on hot reload |

Tampermonkey headers (`==UserScript==`, `@match`, `@exclude`, `@run-at`) and `GM_addStyle / GM_getValue / GM_setValue / GM_xmlhttpRequest / GM_notification / GM_log / unsafeWindow` work too. See [`examples/scripts`](examples/scripts).

## MCP tools

| tool | what |
|---|---|
| `graft_status` | is the extension connected, which mode, where scripts live |
| `graft_list_tabs` | tabs + ids |
| `graft_eval` | run JS in the page, get JSON back (expression or statements + `return`, top-level `await`) |
| `graft_dom` | outline of interactive elements with selectors / html / text |
| `graft_act` | click, type (framework-safe), press, scroll |
| `graft_screenshot` | PNG of the tab |
| `graft_navigate` | go to a URL |
| `graft_get_logs` | console + uncaught errors, or network requests |
| `graft_install_script` | persist a script (hot-reloaded into open tabs) |
| `graft_list_scripts` / `graft_read_script` / `graft_remove_script` | manage scripts |

## How it fits together

```
 coding agent ──stdio/MCP──▢ graft mcp ─┐
                                        β”œβ”€β–Ά hub (127.0.0.1:7777) ──ws──▢ extension ──▢ your tabs
 ~/.graft/scripts/*.graft.js ──watchβ”€β”€β”€β”€β”˜
```

Multiple agents can share one hub: the first process starts it, later ones attach over `/rpc`.

## Security model

- Hub binds `127.0.0.1` only. The extension socket rejects any browser `Origin` other than `chrome-extension://`; `/rpc` rejects every browser origin.
- Privileged calls (`graft.fetch`, `store`, `notify`) cross from the page to the extension over CustomEvents whose names contain a per-session secret. The `graft` object is never exposed on `window`.
- Residual risk: a hostile page that monkey-patches `EventTarget` *before* graft loads could try to sniff that secret. Don't run graft-privileged scripts on pages you don't trust.
- **Scripts you install run with full access to every site they match. Read them.** `graft_eval` gives your agent the same power on any open tab, so only connect agents you trust.
- Optional `debugger` permission (requested from the popup) lets one-off eval work on strict-CSP pages; Chrome shows a "debugging" banner while it runs.

## Limits

Chrome / Chromium (MV3, β‰₯120) only for now. No `chrome://` pages or the Web Store. Page `eval` on strict-CSP sites needs the debugger permission or a persistent script.

## Develop

```bash
npm test                     # protocol smoke test (fake extension)
node scripts/make-icons.mjs  # regenerate extension icons
```

Branding and UI spec for designers: [`docs/DESIGN-BRIEF.md`](docs/DESIGN-BRIEF.md). Landing page: [`docs/index.html`](docs/index.html).

MIT