vite-plugin-specter 0.7.0 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -9
- package/dist/client.js +406 -102
- package/dist/extension-chrome/content.js +406 -102
- package/dist/extension-chrome/manifest.json +1 -1
- package/dist/extension-firefox/content.js +406 -102
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +406 -102
- package/dist/index.js +406 -102
- package/extension/content.js +406 -102
- package/mcp-bridge/README.md +73 -0
- package/mcp-bridge/package.json +10 -0
- package/mcp-bridge/server.mjs +161 -0
- package/package.json +3 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Specter → Claude MCP bridge
|
|
2
|
+
|
|
3
|
+
A tiny, zero-dependency local process so a whole batch of Specter annotations reaches
|
|
4
|
+
Claude Code (or Kiro) with no copy-paste. It:
|
|
5
|
+
|
|
6
|
+
1. Listens on `http://127.0.0.1:8787` — Specter **auto-syncs** its current Specs here
|
|
7
|
+
as you annotate (a fresh snapshot per page; the panel's dot shows ● synced / ○ offline).
|
|
8
|
+
2. Serves the **MCP** protocol over stdio, exposing tools Claude pulls from.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Specter panel ──auto-sync POST──▶ bridge (this process) ──MCP──▶ Claude Code / Kiro
|
|
12
|
+
(green ● dot) mirrors your Specs pop_specs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 1. Turn on auto-sync in Specter
|
|
16
|
+
|
|
17
|
+
In your `vite.config.*`:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { specter } from 'vite-plugin-specter';
|
|
21
|
+
export default { plugins: [specter({ claudeBridge: true })] };
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`claudeBridge: true` targets the default bridge (`http://127.0.0.1:8787`). Pass
|
|
25
|
+
`{ url: 'http://127.0.0.1:9000' }` to override (match `SPECTER_BRIDGE_PORT`).
|
|
26
|
+
|
|
27
|
+
## 2. Register the bridge with your agent
|
|
28
|
+
|
|
29
|
+
**Claude Code** (from your project root):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add specter -- node /absolute/path/to/vite-plugin-specter/mcp-bridge/server.mjs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or add to `.mcp.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"specter": { "command": "node", "args": ["/absolute/path/to/mcp-bridge/server.mjs"] }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Kiro** — add the same `command`/`args` to its MCP config (`mcp.json`).
|
|
46
|
+
|
|
47
|
+
Claude Code launches the process for you (stdio); the HTTP listener comes up
|
|
48
|
+
alongside it. To run it standalone (e.g. to test), `npm start` in this folder.
|
|
49
|
+
|
|
50
|
+
## 3. Use it
|
|
51
|
+
|
|
52
|
+
1. Drop Specs in the browser — they auto-sync (watch the panel's green ● dot).
|
|
53
|
+
2. In the IDE, run **`/spectify`** (or tell Claude *"apply my Specter notes"*).
|
|
54
|
+
Claude reads the synced batch and edits your code. The `/spectify` command lives at
|
|
55
|
+
`~/.claude/commands/spectify.md`.
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| tool | what it does |
|
|
60
|
+
|------|--------------|
|
|
61
|
+
| `pop_specs` | return all pending Specs as one batch, then clear the queue |
|
|
62
|
+
| `peek_specs` | return them without clearing |
|
|
63
|
+
| `clear_specs`| discard pending Specs |
|
|
64
|
+
|
|
65
|
+
## Notes & limits
|
|
66
|
+
|
|
67
|
+
- **Local only.** Binds `127.0.0.1`; nothing leaves your machine.
|
|
68
|
+
- **Pull, not push.** MCP is pull-based — auto-sync keeps the batch *staged*; running
|
|
69
|
+
`/spectify` (or a prompt) is what makes Claude fetch it. There's no way to inject text
|
|
70
|
+
into a live agent session unprompted.
|
|
71
|
+
- **One HTTP owner.** If several agent sessions each spawn the bridge, the first
|
|
72
|
+
to bind `8787` owns the browser endpoint; the rest still serve MCP over stdio.
|
|
73
|
+
- `SPECTER_BRIDGE_PORT` overrides the port (keep it in sync with `claudeBridge.url`).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specter-mcp-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Local bridge that lets Specter batch-send annotations to Claude Code / Kiro over MCP.",
|
|
7
|
+
"bin": { "specter-bridge": "server.mjs" },
|
|
8
|
+
"scripts": { "start": "node server.mjs" },
|
|
9
|
+
"engines": { "node": ">=18" }
|
|
10
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Specter → Claude MCP bridge.
|
|
3
|
+
//
|
|
4
|
+
// One tiny process that does two things at once:
|
|
5
|
+
// 1. Runs an HTTP listener on 127.0.0.1 that Specter auto-syncs its Specs to
|
|
6
|
+
// (a fresh snapshot per page URL, replacing the previous one — it mirrors
|
|
7
|
+
// whatever is currently in the browser panel).
|
|
8
|
+
// 2. Speaks the MCP protocol over stdio so Claude Code / Kiro can pull those
|
|
9
|
+
// Specs with the `pop_specs` tool — no copy-paste, no button.
|
|
10
|
+
//
|
|
11
|
+
// Zero dependencies. stdout is reserved for MCP messages; all logs go to stderr.
|
|
12
|
+
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import readline from 'node:readline';
|
|
15
|
+
|
|
16
|
+
const PORT = Number(process.env.SPECTER_BRIDGE_PORT) || 8787;
|
|
17
|
+
// url -> { url, receivedAt, text, specs[] }. Auto-sync REPLACES a url's snapshot,
|
|
18
|
+
// so the bridge always reflects the browser's current Specs (empty sync clears it).
|
|
19
|
+
let snapshots = {};
|
|
20
|
+
|
|
21
|
+
function log(...a) { console.error('[specter-bridge]', ...a); } // NEVER stdout
|
|
22
|
+
function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); }
|
|
23
|
+
function all() { return Object.values(snapshots); }
|
|
24
|
+
function specCount() { return all().reduce((n, s) => n + s.specs.length, 0); }
|
|
25
|
+
|
|
26
|
+
// ─── HTTP listener (Specter auto-syncs here) ──────────────────────────────────
|
|
27
|
+
const httpServer = http.createServer((req, res) => {
|
|
28
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
29
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
30
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
31
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
32
|
+
|
|
33
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
34
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
35
|
+
res.end(JSON.stringify({ ok: true, sources: all().length, specs: specCount() }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Plain-HTTP read of the staged Specs (peek). Lets any tool — curl, /spectify,
|
|
39
|
+
// a Kiro extension — read the batch without speaking MCP. ?clear=1 consumes.
|
|
40
|
+
if (req.method === 'GET' && (req.url === '/pending' || req.url.startsWith('/pending?'))) {
|
|
41
|
+
// Optional ?url=<substring> filter so ONE bridge can serve many projects:
|
|
42
|
+
// /spectify passes the project's origin/port and only pulls that project's Specs.
|
|
43
|
+
// Empty/missing = no filter (backward compatible).
|
|
44
|
+
const q = req.url.indexOf('?') >= 0 ? new URLSearchParams(req.url.slice(req.url.indexOf('?') + 1)) : null;
|
|
45
|
+
const filter = (q && q.get('url')) || '';
|
|
46
|
+
const match = (b) => !filter || (b.url || '').indexOf(filter) >= 0;
|
|
47
|
+
// Lean payload for /spectify: each spec's `body` already carries the note-less
|
|
48
|
+
// properties + the greppable `find:` anchor, so drop the batch-level `text`
|
|
49
|
+
// (a full duplicate of every body) to avoid shipping the same data twice.
|
|
50
|
+
const batches = all().filter(match).map((b) => ({ url: b.url, receivedAt: b.receivedAt, specs: b.specs }));
|
|
51
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
52
|
+
res.end(JSON.stringify({ ok: true, batches }));
|
|
53
|
+
// ?clear=1 consumes only what was returned — a filtered clear leaves other projects intact.
|
|
54
|
+
if (req.url.indexOf('clear=1') >= 0) {
|
|
55
|
+
if (filter) { all().filter(match).forEach((b) => { delete snapshots[b.url]; }); }
|
|
56
|
+
else snapshots = {};
|
|
57
|
+
log(`GET ${req.url} → returned + cleared${filter ? ' (filtered: ' + filter + ')' : ''}`);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (req.method === 'POST' && req.url === '/specs') {
|
|
62
|
+
let body = '';
|
|
63
|
+
req.on('data', (c) => { body += c; if (body.length > 5e6) req.destroy(); });
|
|
64
|
+
req.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(body || '{}');
|
|
67
|
+
const url = data.url || 'default';
|
|
68
|
+
const specs = Array.isArray(data.specs) ? data.specs : [];
|
|
69
|
+
if (specs.length) snapshots[url] = { url, receivedAt: new Date().toISOString(), text: String(data.text || ''), specs };
|
|
70
|
+
else delete snapshots[url]; // empty sync = the page has no Specs anymore
|
|
71
|
+
log(`sync from ${url}: ${specs.length} Spec(s) (${specCount()} total across ${all().length} source(s))`);
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify({ ok: true, specs: specCount() }));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
76
|
+
res.end(JSON.stringify({ ok: false, error: 'bad json' }));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.writeHead(404); res.end();
|
|
82
|
+
});
|
|
83
|
+
httpServer.on('error', (e) => {
|
|
84
|
+
if (e.code === 'EADDRINUSE') log(`port ${PORT} already in use — another bridge owns the HTTP listener; MCP still served over stdio.`);
|
|
85
|
+
else log('HTTP error:', e.message);
|
|
86
|
+
});
|
|
87
|
+
httpServer.listen(PORT, '127.0.0.1', () => log(`HTTP listening on http://127.0.0.1:${PORT} (POST /specs, GET /pending, GET /health)`));
|
|
88
|
+
|
|
89
|
+
// ─── MCP over stdio (newline-delimited JSON-RPC 2.0) ──────────────────────────
|
|
90
|
+
const TOOLS = [
|
|
91
|
+
{
|
|
92
|
+
name: 'pop_specs',
|
|
93
|
+
description: 'Return ALL Specter annotations currently synced from the browser (each with its note, element selector, and captured properties), then clear them. Call this when the user says to apply their Specter notes / Specs / annotations.',
|
|
94
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'peek_specs',
|
|
98
|
+
description: 'Return the currently synced Specter annotations WITHOUT clearing them (preview).',
|
|
99
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'clear_specs',
|
|
103
|
+
description: 'Discard the currently synced Specter annotations.',
|
|
104
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
function renderPending() {
|
|
109
|
+
const batches = all();
|
|
110
|
+
if (!batches.length) return 'No Specter annotations are synced. In the browser: activate Specter (Ctrl+Option+Z) and drop some Specs — they auto-sync here. Then run /spectify.';
|
|
111
|
+
return batches.map((b) => {
|
|
112
|
+
const head = `# Specter — ${b.specs.length} Spec(s)${b.url && b.url !== 'default' ? ' from ' + b.url : ''}`;
|
|
113
|
+
return head + '\n\n' + (b.text || b.specs.map((s, i) => `#${s.num ?? i + 1} ${s.note || '(no note)'}\n${s.body || ''}`).join('\n\n'));
|
|
114
|
+
}).join('\n\n' + '─'.repeat(40) + '\n\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handle(msg) {
|
|
118
|
+
const { id, method, params } = msg;
|
|
119
|
+
const hasId = id !== undefined && id !== null;
|
|
120
|
+
|
|
121
|
+
if (method === 'initialize') {
|
|
122
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
123
|
+
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
124
|
+
capabilities: { tools: {} },
|
|
125
|
+
serverInfo: { name: 'specter-bridge', version: '0.1.0' },
|
|
126
|
+
} });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (method && method.startsWith('notifications/')) return; // no response
|
|
130
|
+
if (method === 'ping') { if (hasId) send({ jsonrpc: '2.0', id, result: {} }); return; }
|
|
131
|
+
if (method === 'tools/list') { send({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); return; }
|
|
132
|
+
if (method === 'tools/call') {
|
|
133
|
+
const name = params && params.name;
|
|
134
|
+
if (name === 'pop_specs' || name === 'peek_specs') {
|
|
135
|
+
const text = renderPending();
|
|
136
|
+
if (name === 'pop_specs') { log(`pop_specs → returned + cleared ${specCount()} Spec(s)`); snapshots = {}; }
|
|
137
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (name === 'clear_specs') {
|
|
141
|
+
const n = specCount(); snapshots = {};
|
|
142
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `Cleared ${n} synced Spec(s).` }] } });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Unknown tool: ' + name } });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (hasId) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
152
|
+
rl.on('line', (line) => {
|
|
153
|
+
line = line.trim();
|
|
154
|
+
if (!line) return;
|
|
155
|
+
let msg;
|
|
156
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
157
|
+
try { handle(msg); } catch (e) { log('handler error:', e.message); }
|
|
158
|
+
});
|
|
159
|
+
rl.on('close', () => { httpServer.close(); process.exit(0); });
|
|
160
|
+
|
|
161
|
+
log('MCP stdio ready — waiting for Claude to connect');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-specter",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Inspect elements and Figma-style measure spacing in your vibe-coded Vite projects. Give your AI exactly what it needs to make the right change.",
|
|
5
5
|
"author": "Setu Kathawate <dev@setugk.com>",
|
|
6
6
|
"homepage": "https://github.com/setugk/vite-plugin-specter#readme",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"extension"
|
|
27
|
+
"extension",
|
|
28
|
+
"mcp-bridge"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "tsup && node scripts/build-client.mjs && node scripts/build-extension.mjs",
|