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.
@@ -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.0",
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",