@spooky-sync/devtools-mcp 0.0.1-canary.64 → 0.0.1-canary.65
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/AGENTS.md +62 -0
- package/dist/server.js +86 -0
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `@spooky-sync/devtools-mcp` (`sp00ky-mcp`) — agent guide
|
|
2
|
+
|
|
3
|
+
## What this package is
|
|
4
|
+
|
|
5
|
+
A Model Context Protocol server that gives an AI assistant live, structured access to a running sp00ky app. It bridges to either:
|
|
6
|
+
|
|
7
|
+
1. **The Sp00ky DevTools browser extension** (via WebSocket on the side channel `bridge.ts` opens), so the assistant sees the *exact* state the user's tab has — local cache, active live queries, event history, auth state.
|
|
8
|
+
2. **A direct SurrealDB connection** (set `SURREAL_URL`, `SURREAL_USER`, `SURREAL_PASS`, `SURREAL_NS`, `SURREAL_DB`) — useful when there's no browser tab attached.
|
|
9
|
+
|
|
10
|
+
Most tools transparently fall through: extension first, raw DB as a fallback. A handful (auth state, event history clear) require the extension.
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @spooky-sync/devtools-mcp
|
|
16
|
+
# or, from inside an app, via the CLI passthrough:
|
|
17
|
+
spky mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Wire into Claude / your agent via the MCP server config (`.mcp.json` at the repo root or your client's equivalent).
|
|
21
|
+
|
|
22
|
+
## Tools exposed
|
|
23
|
+
|
|
24
|
+
### Connection / state
|
|
25
|
+
|
|
26
|
+
- **`list_connections`** — which browser tabs are currently bridged.
|
|
27
|
+
- **`get_state` `[tabId]`** — full devtools state: events, queries, auth, database tables (extension only).
|
|
28
|
+
- **`get_auth_state` `[tabId]`** — current auth subject + scope (extension only).
|
|
29
|
+
- **`get_active_queries` `[tabId]`** — registered live queries and their last result hashes.
|
|
30
|
+
- **`get_events` `[eventType] [limit]`** — recent event log, optionally filtered.
|
|
31
|
+
- **`clear_history` `[tabId]`** — wipe the in-tab event log (extension only).
|
|
32
|
+
|
|
33
|
+
### Database (works against extension OR direct DB)
|
|
34
|
+
|
|
35
|
+
- **`run_query` `query` `[target=local|remote]` `[tabId]`** — execute arbitrary SurQL.
|
|
36
|
+
- **`list_tables` `[tabId]`** — names of tables defined in the running schema.
|
|
37
|
+
- **`get_table_data` `tableName` `[limit]` `[tabId]`** — `SELECT * LIMIT n` shortcut.
|
|
38
|
+
- **`update_table_row` `recordId` `updates` `[tableName] [tabId]`** — `UPDATE recordId MERGE updates`.
|
|
39
|
+
- **`delete_table_row` `recordId` `[tableName] [tabId]`** — `DELETE recordId`.
|
|
40
|
+
|
|
41
|
+
### Resources
|
|
42
|
+
|
|
43
|
+
- `sp00ky://state` — full state JSON (extension only).
|
|
44
|
+
- `sp00ky://tables` — table list.
|
|
45
|
+
|
|
46
|
+
## How an agent should use this
|
|
47
|
+
|
|
48
|
+
- **Before writing a query against a schema you haven't seen:** call `list_tables` and `get_table_data` with `limit: 1` on each table to learn the columns.
|
|
49
|
+
- **After making a mutation through `db.update`:** call `get_active_queries` or `get_events` to confirm the mutation drained and any subscribed queries refreshed.
|
|
50
|
+
- **When troubleshooting "why isn't this query updating?":** `get_active_queries` shows the registered SurQL and last result hash; `get_events` shows whether new records ingested.
|
|
51
|
+
- **For schema spelunking from outside the browser:** point `SURREAL_URL` at the dev DB and use `run_query` with `INFO FOR DB;`.
|
|
52
|
+
|
|
53
|
+
## Common gotchas
|
|
54
|
+
|
|
55
|
+
- **Mutations through this MCP bypass the local mutation queue.** `update_table_row` calls remote SurrealDB directly (or the extension's bridge). The local cache will see the change only when SSP pushes the live update back. For testing in-app, prefer driving mutations through the page itself.
|
|
56
|
+
- **Tab selection matters.** Multi-tab dev: pass `tabId` explicitly or you get whichever tab connected first.
|
|
57
|
+
- **Direct-DB fallback has a smaller capability set.** Anything that reads in-memory devtools state (auth, events, active queries) only works with the extension.
|
|
58
|
+
|
|
59
|
+
## Pointers
|
|
60
|
+
|
|
61
|
+
- Sync engine that emits the events this server reads: `node_modules/@spooky-sync/core/AGENTS.md` → `src/modules/devtools/`
|
|
62
|
+
- CLI passthrough: `node_modules/@spooky-sync/cli/AGENTS.md` (`spky mcp`)
|
package/dist/server.js
CHANGED
|
@@ -142,6 +142,92 @@ export function createServer(bridge, surreal) {
|
|
|
142
142
|
await bridge.request(BRIDGE_METHODS.CLEAR_HISTORY, {}, tabId);
|
|
143
143
|
return { content: [{ type: 'text', text: 'History cleared.' }] };
|
|
144
144
|
});
|
|
145
|
+
server.tool('describe_schema', 'Describe all tables with columns, types, and sp00ky annotations (@crdt, @parent). Stitches INFO FOR DB with parsed schema metadata. With the browser extension this returns @crdt/@parent semantics; direct-DB mode returns raw column info only.', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
|
|
146
|
+
if (bridge.isConnected) {
|
|
147
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
|
|
148
|
+
const dbState = state?.database ?? {};
|
|
149
|
+
return json({
|
|
150
|
+
source: 'extension',
|
|
151
|
+
tables: dbState.tables ?? [],
|
|
152
|
+
relationships: dbState.relationships ?? [],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (surreal) {
|
|
156
|
+
const dbInfo = (await surreal.query('INFO FOR DB;'));
|
|
157
|
+
const tablesObj = dbInfo?.[0]?.result?.tables ?? dbInfo?.[0]?.tables ?? {};
|
|
158
|
+
const tableNames = Object.keys(tablesObj);
|
|
159
|
+
const tables = await Promise.all(tableNames.map(async (name) => {
|
|
160
|
+
try {
|
|
161
|
+
const info = (await surreal.query(`INFO FOR TABLE \`${name}\`;`));
|
|
162
|
+
const fieldsObj = info?.[0]?.result?.fields ?? info?.[0]?.fields ?? {};
|
|
163
|
+
const columns = Object.entries(fieldsObj).map(([fname, def]) => ({
|
|
164
|
+
name: fname,
|
|
165
|
+
definition: typeof def === 'string' ? def : JSON.stringify(def),
|
|
166
|
+
}));
|
|
167
|
+
return { name, columns };
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
return { name, columns: [], error: e instanceof Error ? e.message : String(e) };
|
|
171
|
+
}
|
|
172
|
+
}));
|
|
173
|
+
return json({
|
|
174
|
+
source: 'direct-db',
|
|
175
|
+
note: '@crdt / @parent annotations are not visible in direct-DB mode; connect the browser extension to see them.',
|
|
176
|
+
tables,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
180
|
+
});
|
|
181
|
+
server.tool('lint_query', 'Validate a SurrealQL query without running it. Sends EXPLAIN <query> through the connected channel; returns parse / plan errors with location when SurrealDB provides them.', {
|
|
182
|
+
query: z.string().describe('SurrealQL query to validate'),
|
|
183
|
+
target: z
|
|
184
|
+
.enum(['local', 'remote'])
|
|
185
|
+
.optional()
|
|
186
|
+
.default('remote')
|
|
187
|
+
.describe('When using the extension: lint against local (cache) or remote DB'),
|
|
188
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
189
|
+
}, async ({ query, target, tabId }) => {
|
|
190
|
+
const trimmed = query.trim().replace(/;\s*$/, '');
|
|
191
|
+
const explainQuery = /^\s*EXPLAIN\b/i.test(trimmed) ? trimmed : `EXPLAIN ${trimmed};`;
|
|
192
|
+
const parseError = (msg) => {
|
|
193
|
+
const m = msg.match(/line\s+(\d+)(?:[,\s]+col(?:umn)?\s+(\d+))?/i);
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
errors: [
|
|
197
|
+
{
|
|
198
|
+
message: msg,
|
|
199
|
+
line: m ? Number(m[1]) : undefined,
|
|
200
|
+
column: m && m[2] ? Number(m[2]) : undefined,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
const inspectResult = (raw) => {
|
|
206
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
207
|
+
const errors = arr
|
|
208
|
+
.map((r) => (r && r.status === 'ERR' ? r.result ?? r.message : null))
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
if (errors.length > 0) {
|
|
211
|
+
return { ok: false, errors: errors.map((m) => parseError(m).errors[0]) };
|
|
212
|
+
}
|
|
213
|
+
return { ok: true, plan: arr };
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
if (bridge.isConnected) {
|
|
217
|
+
const result = await bridge.request(BRIDGE_METHODS.RUN_QUERY, { query: explainQuery, target }, tabId);
|
|
218
|
+
return json(inspectResult(result));
|
|
219
|
+
}
|
|
220
|
+
if (surreal) {
|
|
221
|
+
const result = await surreal.query(explainQuery);
|
|
222
|
+
return json(inspectResult(result));
|
|
223
|
+
}
|
|
224
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
228
|
+
return json(parseError(msg));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
145
231
|
// --- Resources ---
|
|
146
232
|
server.resource('state', 'sp00ky://state', { description: 'Full Sp00ky DevTools state' }, async (uri) => {
|
|
147
233
|
if (!bridge.isConnected) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/devtools-mcp",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.65",
|
|
4
4
|
"description": "MCP server for Sp00ky Sync devtools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"sp00ky-mcp": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"AGENTS.md"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "tsc",
|