vite-plugin-specter 0.7.1 → 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 +246 -28
- package/dist/extension-chrome/content.js +246 -28
- package/dist/extension-chrome/manifest.json +1 -1
- package/dist/extension-firefox/content.js +246 -28
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.js +246 -28
- package/extension/content.js +246 -28
- 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,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",
|