@spooky-sync/devtools-mcp 0.0.1-canary.34 → 0.0.1-canary.37

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/dist/bridge.js CHANGED
@@ -15,7 +15,7 @@ export class Bridge {
15
15
  return Array.from(this.connectedTabs.values());
16
16
  }
17
17
  start() {
18
- const port = parseInt(process.env.SP00KY_MCP_PORT || '', 10) || BRIDGE_PORT;
18
+ const port = Number.parseInt(process.env.SP00KY_MCP_PORT || '', 10) || BRIDGE_PORT;
19
19
  return new Promise((resolve, reject) => {
20
20
  this.wss = new WebSocketServer({ host: '127.0.0.1', port }, () => {
21
21
  process.stderr.write(`[sp00ky-mcp] Bridge listening on ws://127.0.0.1:${port}\n`);
@@ -124,6 +124,7 @@ export class Bridge {
124
124
  reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms: ${method}`));
125
125
  }, REQUEST_TIMEOUT_MS);
126
126
  this.pendingRequests.set(id, { resolve, reject, timer });
127
+ // oxlint-disable-next-line no-non-null-assertion
127
128
  this.extensionSocket.send(JSON.stringify(request));
128
129
  });
129
130
  }
package/dist/index.js CHANGED
@@ -1,11 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { Bridge } from './bridge.js';
4
+ import { SurrealClient } from './surreal.js';
4
5
  import { createServer } from './server.js';
5
6
  async function main() {
6
7
  const bridge = new Bridge();
7
8
  await bridge.start();
8
- const server = createServer(bridge);
9
+ const surreal = process.env.SURREAL_URL
10
+ ? new SurrealClient({
11
+ url: process.env.SURREAL_URL,
12
+ namespace: process.env.SURREAL_NS ?? 'main',
13
+ database: process.env.SURREAL_DB ?? 'main',
14
+ username: process.env.SURREAL_USER ?? 'root',
15
+ password: process.env.SURREAL_PASS ?? 'root',
16
+ })
17
+ : null;
18
+ if (surreal) {
19
+ process.stderr.write(`[sp00ky-mcp] Direct DB mode enabled (${process.env.SURREAL_URL})\n`);
20
+ }
21
+ const server = createServer(bridge, surreal);
9
22
  const transport = new StdioServerTransport();
10
23
  await server.connect(transport);
11
24
  process.stderr.write('[sp00ky-mcp] MCP server running on stdio\n');
package/dist/server.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { Bridge } from './bridge.js';
3
- export declare function createServer(bridge: Bridge): McpServer;
2
+ import type { Bridge } from './bridge.js';
3
+ import type { SurrealClient } from './surreal.js';
4
+ export declare function createServer(bridge: Bridge, surreal?: SurrealClient | null): McpServer;
package/dist/server.js CHANGED
@@ -1,120 +1,204 @@
1
1
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { BRIDGE_METHODS } from './protocol.js';
4
- export function createServer(bridge) {
4
+ function json(data) {
5
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
6
+ }
7
+ export function createServer(bridge, surreal) {
5
8
  const server = new McpServer({
6
9
  name: 'sp00ky-devtools',
7
10
  version: '0.0.1',
8
11
  });
9
12
  // --- Tools ---
10
13
  server.tool('list_connections', 'List browser tabs connected to Sp00ky DevTools', {}, async () => {
11
- const tabs = bridge.getConnectedTabs();
12
- return {
13
- content: [
14
- {
15
- type: 'text',
16
- text: JSON.stringify({
17
- connected: bridge.isConnected,
18
- tabs,
19
- }, null, 2),
20
- },
21
- ],
22
- };
14
+ return json({ connected: bridge.isConnected, tabs: bridge.getConnectedTabs() });
23
15
  });
24
16
  server.tool('get_state', 'Get the full Sp00ky DevTools state (events, queries, auth, database)', { tabId: z.number().optional().describe('Browser tab ID (uses first connected tab if omitted)') }, async ({ tabId }) => {
17
+ if (!bridge.isConnected) {
18
+ throw new Error('No extension connected. get_state requires the Sp00ky DevTools browser extension.');
19
+ }
25
20
  const result = await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId);
26
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
21
+ return json(result);
27
22
  });
28
23
  server.tool('run_query', 'Execute a SurrealQL query against the database', {
29
24
  query: z.string().describe('SurrealQL query to execute'),
30
25
  target: z.enum(['local', 'remote']).optional().default('remote').describe('Query target: local or remote database'),
31
26
  tabId: z.number().optional().describe('Browser tab ID'),
32
27
  }, async ({ query, target, tabId }) => {
33
- const result = await bridge.request(BRIDGE_METHODS.RUN_QUERY, { query, target }, tabId);
34
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
28
+ if (bridge.isConnected) {
29
+ const result = await bridge.request(BRIDGE_METHODS.RUN_QUERY, { query, target }, tabId);
30
+ return json(result);
31
+ }
32
+ if (surreal) {
33
+ const result = await surreal.query(query);
34
+ return json(result);
35
+ }
36
+ throw new Error('No extension connected and no direct database configured. Set SURREAL_URL or connect the browser extension.');
35
37
  });
36
38
  server.tool('list_tables', 'List all database tables', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
37
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
38
- const tables = state?.database?.tables ?? [];
39
- return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
39
+ if (bridge.isConnected) {
40
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
41
+ const tables = state?.database?.tables ?? [];
42
+ return json(tables);
43
+ }
44
+ if (surreal) {
45
+ const result = await surreal.query('INFO FOR DB;');
46
+ const info = result;
47
+ const tables = info?.[0]?.result?.tables ?? info?.[0]?.tables ?? {};
48
+ return json(Object.keys(tables));
49
+ }
50
+ throw new Error('No extension connected and no direct database configured.');
40
51
  });
41
- server.tool('get_table_data', 'Fetch all records from a database table', {
52
+ server.tool('get_table_data', 'Fetch records from a database table', {
42
53
  tableName: z.string().describe('Name of the table'),
54
+ limit: z.number().optional().default(100).describe('Max number of records to return'),
43
55
  tabId: z.number().optional().describe('Browser tab ID'),
44
- }, async ({ tableName, tabId }) => {
45
- const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName }, tabId);
46
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
56
+ }, async ({ tableName, limit, tabId }) => {
57
+ if (bridge.isConnected) {
58
+ const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName }, tabId);
59
+ return json(result);
60
+ }
61
+ if (surreal) {
62
+ const result = await surreal.query(`SELECT * FROM \`${tableName}\` LIMIT ${limit};`);
63
+ return json(result);
64
+ }
65
+ throw new Error('No extension connected and no direct database configured.');
47
66
  });
48
67
  server.tool('update_table_row', 'Update a record in a database table', {
49
- tableName: z.string().describe('Name of the table'),
50
- recordId: z.string().describe('Record ID to update'),
68
+ tableName: z.string().optional().describe('Name of the table (used when browser extension is connected)'),
69
+ recordId: z.string().describe('Record ID to update (e.g. "users:abc123")'),
51
70
  updates: z.record(z.unknown()).describe('Fields to update'),
52
71
  tabId: z.number().optional().describe('Browser tab ID'),
53
72
  }, async ({ tableName, recordId, updates, tabId }) => {
54
- const result = await bridge.request(BRIDGE_METHODS.UPDATE_TABLE_ROW, { tableName, recordId, updates }, tabId);
55
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
73
+ if (bridge.isConnected) {
74
+ const result = await bridge.request(BRIDGE_METHODS.UPDATE_TABLE_ROW, { tableName, recordId, updates }, tabId);
75
+ return json(result);
76
+ }
77
+ if (surreal) {
78
+ const result = await surreal.query(`UPDATE ${recordId} MERGE ${JSON.stringify(updates)};`);
79
+ return json(result);
80
+ }
81
+ throw new Error('No extension connected and no direct database configured.');
56
82
  });
57
83
  server.tool('delete_table_row', 'Delete a record from a database table', {
58
- tableName: z.string().describe('Name of the table'),
59
- recordId: z.string().describe('Record ID to delete'),
84
+ tableName: z.string().optional().describe('Name of the table (used when browser extension is connected)'),
85
+ recordId: z.string().describe('Record ID to delete (e.g. "users:abc123")'),
60
86
  tabId: z.number().optional().describe('Browser tab ID'),
61
87
  }, async ({ tableName, recordId, tabId }) => {
62
- const result = await bridge.request(BRIDGE_METHODS.DELETE_TABLE_ROW, { tableName, recordId }, tabId);
63
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
88
+ if (bridge.isConnected) {
89
+ const result = await bridge.request(BRIDGE_METHODS.DELETE_TABLE_ROW, { tableName, recordId }, tabId);
90
+ return json(result);
91
+ }
92
+ if (surreal) {
93
+ const result = await surreal.query(`DELETE ${recordId};`);
94
+ return json(result);
95
+ }
96
+ throw new Error('No extension connected and no direct database configured.');
64
97
  });
65
98
  server.tool('get_active_queries', 'Get all active live queries and their data', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
66
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
67
- const queries = state?.activeQueries ?? [];
68
- return { content: [{ type: 'text', text: JSON.stringify(queries, null, 2) }] };
99
+ if (bridge.isConnected) {
100
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
101
+ return json(state?.activeQueries ?? []);
102
+ }
103
+ if (surreal) {
104
+ const result = await surreal.query('SELECT * FROM _00_query;');
105
+ return json(result);
106
+ }
107
+ throw new Error('No extension connected and no direct database configured.');
69
108
  });
70
109
  server.tool('get_events', 'Get event history, optionally filtered by type', {
71
110
  eventType: z.string().optional().describe('Filter by event type'),
72
- limit: z.number().optional().describe('Max number of events to return'),
111
+ limit: z.number().optional().default(50).describe('Max number of events to return'),
73
112
  tabId: z.number().optional().describe('Browser tab ID'),
74
113
  }, async ({ eventType, limit, tabId }) => {
75
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
76
- let events = state?.eventsHistory ?? [];
77
- if (eventType) {
78
- events = events.filter((e) => e.eventType === eventType);
114
+ if (bridge.isConnected) {
115
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
116
+ let events = state?.eventsHistory ?? [];
117
+ if (eventType) {
118
+ events = events.filter((e) => e.eventType === eventType);
119
+ }
120
+ if (limit) {
121
+ events = events.slice(-limit);
122
+ }
123
+ return json(events);
79
124
  }
80
- if (limit) {
81
- events = events.slice(-limit);
125
+ if (surreal) {
126
+ const result = await surreal.query(`SELECT * FROM _00_events ORDER BY timestamp DESC LIMIT ${limit};`);
127
+ return json(result);
82
128
  }
83
- return { content: [{ type: 'text', text: JSON.stringify(events, null, 2) }] };
129
+ throw new Error('No extension connected and no direct database configured.');
84
130
  });
85
131
  server.tool('get_auth_state', 'Get the current authentication state', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
132
+ if (!bridge.isConnected) {
133
+ throw new Error('No extension connected. get_auth_state requires the Sp00ky DevTools browser extension.');
134
+ }
86
135
  const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
87
- const auth = state?.auth ?? null;
88
- return { content: [{ type: 'text', text: JSON.stringify(auth, null, 2) }] };
136
+ return json(state?.auth ?? null);
89
137
  });
90
138
  server.tool('clear_history', 'Clear the event history', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
139
+ if (!bridge.isConnected) {
140
+ throw new Error('No extension connected. clear_history requires the Sp00ky DevTools browser extension.');
141
+ }
91
142
  await bridge.request(BRIDGE_METHODS.CLEAR_HISTORY, {}, tabId);
92
143
  return { content: [{ type: 'text', text: 'History cleared.' }] };
93
144
  });
94
145
  // --- Resources ---
95
146
  server.resource('state', 'sp00ky://state', { description: 'Full Sp00ky DevTools state' }, async (uri) => {
147
+ if (!bridge.isConnected) {
148
+ throw new Error('No extension connected. State resource requires the browser extension.');
149
+ }
96
150
  const state = await bridge.request(BRIDGE_METHODS.GET_STATE);
97
151
  return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(state, null, 2) }] };
98
152
  });
99
153
  server.resource('tables', 'sp00ky://tables', { description: 'List of database tables' }, async (uri) => {
100
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
101
- const tables = state?.database?.tables ?? [];
102
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(tables, null, 2) }] };
154
+ if (bridge.isConnected) {
155
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
156
+ const tables = state?.database?.tables ?? [];
157
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(tables, null, 2) }] };
158
+ }
159
+ if (surreal) {
160
+ const result = await surreal.query('INFO FOR DB;');
161
+ const info = result;
162
+ const tables = info?.[0]?.result?.tables ?? info?.[0]?.tables ?? {};
163
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(Object.keys(tables), null, 2) }] };
164
+ }
165
+ throw new Error('No extension connected and no direct database configured.');
103
166
  });
104
167
  server.resource('table-data', new ResourceTemplate('sp00ky://tables/{tableName}', { list: undefined }), { description: 'Contents of a specific database table' }, async (uri, variables) => {
105
168
  const tableName = variables.tableName;
106
- const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName });
107
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
169
+ if (bridge.isConnected) {
170
+ const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName });
171
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
172
+ }
173
+ if (surreal) {
174
+ const result = await surreal.query(`SELECT * FROM \`${tableName}\` LIMIT 100;`);
175
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
176
+ }
177
+ throw new Error('No extension connected and no direct database configured.');
108
178
  });
109
179
  server.resource('queries', 'sp00ky://queries', { description: 'Active live queries' }, async (uri) => {
110
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
111
- const queries = state?.activeQueries ?? [];
112
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(queries, null, 2) }] };
180
+ if (bridge.isConnected) {
181
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
182
+ const queries = state?.activeQueries ?? [];
183
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(queries, null, 2) }] };
184
+ }
185
+ if (surreal) {
186
+ const result = await surreal.query('SELECT * FROM _00_query;');
187
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
188
+ }
189
+ throw new Error('No extension connected and no direct database configured.');
113
190
  });
114
191
  server.resource('events', 'sp00ky://events', { description: 'Event history' }, async (uri) => {
115
- const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
116
- const events = state?.eventsHistory ?? [];
117
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(events, null, 2) }] };
192
+ if (bridge.isConnected) {
193
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
194
+ const events = state?.eventsHistory ?? [];
195
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(events, null, 2) }] };
196
+ }
197
+ if (surreal) {
198
+ const result = await surreal.query('SELECT * FROM _00_events ORDER BY timestamp DESC LIMIT 50;');
199
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
200
+ }
201
+ throw new Error('No extension connected and no direct database configured.');
118
202
  });
119
203
  return server;
120
204
  }
@@ -0,0 +1,13 @@
1
+ export interface SurrealConfig {
2
+ url: string;
3
+ namespace: string;
4
+ database: string;
5
+ username: string;
6
+ password: string;
7
+ }
8
+ export declare class SurrealClient {
9
+ private config;
10
+ private authHeader;
11
+ constructor(config: SurrealConfig);
12
+ query(surql: string): Promise<unknown[]>;
13
+ }
@@ -0,0 +1,27 @@
1
+ export class SurrealClient {
2
+ config;
3
+ authHeader;
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.authHeader =
7
+ 'Basic ' + Buffer.from(`${config.username}:${config.password}`).toString('base64');
8
+ }
9
+ async query(surql) {
10
+ const res = await fetch(`${this.config.url}/sql`, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ Authorization: this.authHeader,
15
+ 'surreal-ns': this.config.namespace,
16
+ 'surreal-db': this.config.database,
17
+ Accept: 'application/json',
18
+ },
19
+ body: surql,
20
+ });
21
+ if (!res.ok) {
22
+ const text = await res.text();
23
+ throw new Error(`SurrealDB query failed (${res.status}): ${text}`);
24
+ }
25
+ return res.json();
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/devtools-mcp",
3
- "version": "0.0.1-canary.34",
3
+ "version": "0.0.1-canary.37",
4
4
  "description": "MCP server for Sp00ky Sync devtools",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,8 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "build": "tsc",
15
- "dev": "tsc --watch"
15
+ "dev": "tsc --watch",
16
+ "test": "vitest run"
16
17
  },
17
18
  "repository": {
18
19
  "type": "git",
@@ -29,6 +30,7 @@
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/ws": "^8.18.0",
32
- "typescript": "^5.6.2"
33
+ "typescript": "^5.6.2",
34
+ "vitest": "^1.0.0"
33
35
  }
34
36
  }