blockwerk-mcp 0.1.0

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/index.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BlockWerk MCP Server
4
+ *
5
+ * Bridges an external AI client (Claude Desktop, Cursor) to a live BlockWerk
6
+ * browser tab via the Netlify relay.
7
+ *
8
+ * Usage:
9
+ * npx @blockwerk/mcp --session bw-a1b2c3d4
10
+ * npx @blockwerk/mcp --session bw-a1b2c3d4 --relay https://blockwerk.tech
11
+ */
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { z } from 'zod';
15
+ // ── Config ────────────────────────────────────────────────────────────────────
16
+ const args = process.argv.slice(2);
17
+ const getArg = (flag) => {
18
+ const i = args.indexOf(flag);
19
+ return i !== -1 ? args[i + 1] : undefined;
20
+ };
21
+ const SESSION_ID = getArg('--session') ?? process.env.BLOCKWERK_SESSION;
22
+ const RELAY_BASE = (getArg('--relay') ?? process.env.BLOCKWERK_RELAY ?? 'https://blockwerk.tech').replace(/\/$/, '');
23
+ const RELAY_URL = `${RELAY_BASE}/api/mcp-relay`;
24
+ if (!SESSION_ID) {
25
+ console.error('Error: session ID required.\n' +
26
+ 'Usage: npx @blockwerk/mcp --session <session-id>\n\n' +
27
+ 'Get your session ID from blockwerk.tech → AI panel → "Connect external AI".');
28
+ process.exit(1);
29
+ }
30
+ // ── Relay communication ───────────────────────────────────────────────────────
31
+ const MAX_WAIT_MS = 60000; // total wait before giving up
32
+ async function callBlockWerk(name, input) {
33
+ const requestId = crypto.randomUUID();
34
+ // 1. Push command to relay
35
+ const pushRes = await fetch(`${RELAY_URL}?session=${SESSION_ID}&type=command`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ requestId, name, input }),
39
+ });
40
+ if (!pushRes.ok) {
41
+ throw new Error(`Relay rejected command: ${pushRes.status} ${await pushRes.text()}`);
42
+ }
43
+ // 2. Poll for result — relay returns after ~7s if not yet ready (pending: true)
44
+ // We retry until MAX_WAIT_MS is exceeded.
45
+ const deadline = Date.now() + MAX_WAIT_MS;
46
+ while (Date.now() < deadline) {
47
+ const pollRes = await fetch(`${RELAY_URL}?session=${SESSION_ID}&type=result&requestId=${requestId}`);
48
+ if (!pollRes.ok) {
49
+ throw new Error(`Relay poll failed: ${pollRes.status}`);
50
+ }
51
+ const data = (await pollRes.json());
52
+ if (!data.pending) {
53
+ return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
54
+ }
55
+ // pending: true — relay timed out but result not yet ready, retry immediately
56
+ }
57
+ throw new Error('BlockWerk did not respond within 60 seconds. Is the browser tab open and the MCP bridge active?');
58
+ }
59
+ // ── MCP server ────────────────────────────────────────────────────────────────
60
+ const server = new McpServer({
61
+ name: 'blockwerk',
62
+ version: '0.1.0',
63
+ });
64
+ // ── Tools ─────────────────────────────────────────────────────────────────────
65
+ server.tool('describe_canvas', 'Returns a structured text description of all blocks and connections on the BlockWerk canvas.', {}, async () => ({
66
+ content: [{ type: 'text', text: await callBlockWerk('describe_canvas', {}) }],
67
+ }));
68
+ server.tool('get_available_blocks', 'Lists all available block types with their port IDs and configurable parameters.', {}, async () => ({
69
+ content: [{ type: 'text', text: await callBlockWerk('get_available_blocks', {}) }],
70
+ }));
71
+ server.tool('batch_commands', 'Executes multiple canvas operations in a single round-trip. Use handles (temporary IDs) to reference newly added blocks within the same batch. This is the preferred tool for building diagrams.', {
72
+ commands: z.array(z.object({
73
+ type: z.enum(['add_block', 'connect_blocks', 'update_params', 'delete_block', 'tidy_layout']),
74
+ blockType: z.string().optional().describe('Block type for add_block, e.g. PIDController'),
75
+ x: z.number().optional().describe('X position in pixels'),
76
+ y: z.number().optional().describe('Y position in pixels'),
77
+ handle: z.string().optional().describe('Temporary ID for this block, usable in the same batch'),
78
+ fromBlockId: z.string().optional().describe('Block ID or handle for connect_blocks'),
79
+ fromPortId: z.string().optional().describe('Output port, e.g. "out"'),
80
+ toBlockId: z.string().optional().describe('Block ID or handle for connect_blocks'),
81
+ toPortId: z.string().optional().describe('Input port, e.g. "in"'),
82
+ blockId: z.string().optional().describe('Block ID or handle for update_params / delete_block'),
83
+ params: z.record(z.unknown()).optional().describe('Parameter key-value pairs for update_params'),
84
+ })).describe('Ordered list of commands to execute'),
85
+ }, async ({ commands }) => ({
86
+ content: [{ type: 'text', text: await callBlockWerk('batch_commands', { commands }) }],
87
+ }));
88
+ server.tool('tidy_layout', 'Automatically rearranges all blocks into a clean left-to-right signal flow layout.', {}, async () => ({
89
+ content: [{ type: 'text', text: await callBlockWerk('tidy_layout', {}) }],
90
+ }));
91
+ server.tool('clear_canvas', 'Removes all blocks and connections from the canvas.', {}, async () => ({
92
+ content: [{ type: 'text', text: await callBlockWerk('clear_canvas', {}) }],
93
+ }));
94
+ server.tool('add_block', 'Adds a single block to the canvas. Prefer batch_commands when adding multiple blocks.', {
95
+ type: z.string().describe('Block type, e.g. Integrator, PIDController, Scope'),
96
+ x: z.number().describe('X position in pixels (0–2000)'),
97
+ y: z.number().describe('Y position in pixels (0–2000)'),
98
+ }, async ({ type, x, y }) => ({
99
+ content: [{ type: 'text', text: await callBlockWerk('add_block', { type, x, y }) }],
100
+ }));
101
+ server.tool('connect_blocks', 'Connects an output port of one block to an input port of another.', {
102
+ fromBlockId: z.string().describe('Source block ID'),
103
+ fromPortId: z.string().describe('Output port name, e.g. "out"'),
104
+ toBlockId: z.string().describe('Target block ID'),
105
+ toPortId: z.string().describe('Input port name, e.g. "in"'),
106
+ }, async ({ fromBlockId, fromPortId, toBlockId, toPortId }) => ({
107
+ content: [{ type: 'text', text: await callBlockWerk('connect_blocks', { fromBlockId, fromPortId, toBlockId, toPortId }) }],
108
+ }));
109
+ server.tool('update_params', 'Updates parameters of a block.', {
110
+ blockId: z.string().describe('Block ID to update'),
111
+ params: z.record(z.unknown()).describe('Parameter key-value pairs, e.g. { "value": 1.0 }'),
112
+ }, async ({ blockId, params }) => ({
113
+ content: [{ type: 'text', text: await callBlockWerk('update_params', { blockId, params }) }],
114
+ }));
115
+ server.tool('delete_block', 'Deletes a block and all its connections.', {
116
+ blockId: z.string().describe('Block ID to delete'),
117
+ }, async ({ blockId }) => ({
118
+ content: [{ type: 'text', text: await callBlockWerk('delete_block', { blockId }) }],
119
+ }));
120
+ server.tool('load_diagram', 'Loads a complete BlockWerk project from a JSON string.', {
121
+ json: z.string().describe('JSON project data (from exportJson or a saved file)'),
122
+ }, async ({ json }) => ({
123
+ content: [{ type: 'text', text: await callBlockWerk('load_diagram', { json }) }],
124
+ }));
125
+ // ── Start ─────────────────────────────────────────────────────────────────────
126
+ const transport = new StdioServerTransport();
127
+ await server.connect(transport);
128
+ console.error(`[BlockWerk MCP] Connected — session: ${SESSION_ID}, relay: ${RELAY_URL}`);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "blockwerk-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for BlockWerk — lets Claude Desktop and Cursor control a live BlockWerk browser tab",
5
+ "type": "module",
6
+ "bin": {
7
+ "blockwerk-mcp": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.15.0",
16
+ "zod": "^3.25.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "tsx": "^4.19.0",
21
+ "typescript": "~5.9.3"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BlockWerk MCP Server
4
+ *
5
+ * Bridges an external AI client (Claude Desktop, Cursor) to a live BlockWerk
6
+ * browser tab via the Netlify relay.
7
+ *
8
+ * Usage:
9
+ * npx @blockwerk/mcp --session bw-a1b2c3d4
10
+ * npx @blockwerk/mcp --session bw-a1b2c3d4 --relay https://blockwerk.tech
11
+ */
12
+
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { z } from 'zod';
16
+
17
+ // ── Config ────────────────────────────────────────────────────────────────────
18
+
19
+ const args = process.argv.slice(2);
20
+ const getArg = (flag: string) => {
21
+ const i = args.indexOf(flag);
22
+ return i !== -1 ? args[i + 1] : undefined;
23
+ };
24
+
25
+ const SESSION_ID = getArg('--session') ?? process.env.BLOCKWERK_SESSION;
26
+ const RELAY_BASE = (getArg('--relay') ?? process.env.BLOCKWERK_RELAY ?? 'https://blockwerk.tech').replace(/\/$/, '');
27
+ const RELAY_URL = `${RELAY_BASE}/api/mcp-relay`;
28
+
29
+ if (!SESSION_ID) {
30
+ console.error(
31
+ 'Error: session ID required.\n' +
32
+ 'Usage: npx @blockwerk/mcp --session <session-id>\n\n' +
33
+ 'Get your session ID from blockwerk.tech → AI panel → "Connect external AI".',
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ // ── Relay communication ───────────────────────────────────────────────────────
39
+
40
+ const MAX_WAIT_MS = 60000; // total wait before giving up
41
+
42
+ async function callBlockWerk(name: string, input: Record<string, unknown>): Promise<string> {
43
+ const requestId = crypto.randomUUID();
44
+
45
+ // 1. Push command to relay
46
+ const pushRes = await fetch(`${RELAY_URL}?session=${SESSION_ID}&type=command`, {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ requestId, name, input }),
50
+ });
51
+
52
+ if (!pushRes.ok) {
53
+ throw new Error(`Relay rejected command: ${pushRes.status} ${await pushRes.text()}`);
54
+ }
55
+
56
+ // 2. Poll for result — relay returns after ~7s if not yet ready (pending: true)
57
+ // We retry until MAX_WAIT_MS is exceeded.
58
+ const deadline = Date.now() + MAX_WAIT_MS;
59
+
60
+ while (Date.now() < deadline) {
61
+ const pollRes = await fetch(
62
+ `${RELAY_URL}?session=${SESSION_ID}&type=result&requestId=${requestId}`,
63
+ );
64
+
65
+ if (!pollRes.ok) {
66
+ throw new Error(`Relay poll failed: ${pollRes.status}`);
67
+ }
68
+
69
+ const data = (await pollRes.json()) as { pending?: boolean; result?: unknown };
70
+
71
+ if (!data.pending) {
72
+ return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
73
+ }
74
+
75
+ // pending: true — relay timed out but result not yet ready, retry immediately
76
+ }
77
+
78
+ throw new Error(
79
+ 'BlockWerk did not respond within 60 seconds. Is the browser tab open and the MCP bridge active?',
80
+ );
81
+ }
82
+
83
+ // ── MCP server ────────────────────────────────────────────────────────────────
84
+
85
+ const server = new McpServer({
86
+ name: 'blockwerk',
87
+ version: '0.1.0',
88
+ });
89
+
90
+ // ── Tools ─────────────────────────────────────────────────────────────────────
91
+
92
+ server.tool(
93
+ 'describe_canvas',
94
+ 'Returns a structured text description of all blocks and connections on the BlockWerk canvas.',
95
+ {},
96
+ async () => ({
97
+ content: [{ type: 'text', text: await callBlockWerk('describe_canvas', {}) }],
98
+ }),
99
+ );
100
+
101
+ server.tool(
102
+ 'get_available_blocks',
103
+ 'Lists all available block types with their port IDs and configurable parameters.',
104
+ {},
105
+ async () => ({
106
+ content: [{ type: 'text', text: await callBlockWerk('get_available_blocks', {}) }],
107
+ }),
108
+ );
109
+
110
+ server.tool(
111
+ 'batch_commands',
112
+ 'Executes multiple canvas operations in a single round-trip. Use handles (temporary IDs) to reference newly added blocks within the same batch. This is the preferred tool for building diagrams.',
113
+ {
114
+ commands: z.array(z.object({
115
+ type: z.enum(['add_block', 'connect_blocks', 'update_params', 'delete_block', 'tidy_layout']),
116
+ blockType: z.string().optional().describe('Block type for add_block, e.g. PIDController'),
117
+ x: z.number().optional().describe('X position in pixels'),
118
+ y: z.number().optional().describe('Y position in pixels'),
119
+ handle: z.string().optional().describe('Temporary ID for this block, usable in the same batch'),
120
+ fromBlockId: z.string().optional().describe('Block ID or handle for connect_blocks'),
121
+ fromPortId: z.string().optional().describe('Output port, e.g. "out"'),
122
+ toBlockId: z.string().optional().describe('Block ID or handle for connect_blocks'),
123
+ toPortId: z.string().optional().describe('Input port, e.g. "in"'),
124
+ blockId: z.string().optional().describe('Block ID or handle for update_params / delete_block'),
125
+ params: z.record(z.unknown()).optional().describe('Parameter key-value pairs for update_params'),
126
+ })).describe('Ordered list of commands to execute'),
127
+ },
128
+ async ({ commands }) => ({
129
+ content: [{ type: 'text', text: await callBlockWerk('batch_commands', { commands }) }],
130
+ }),
131
+ );
132
+
133
+ server.tool(
134
+ 'tidy_layout',
135
+ 'Automatically rearranges all blocks into a clean left-to-right signal flow layout.',
136
+ {},
137
+ async () => ({
138
+ content: [{ type: 'text', text: await callBlockWerk('tidy_layout', {}) }],
139
+ }),
140
+ );
141
+
142
+ server.tool(
143
+ 'clear_canvas',
144
+ 'Removes all blocks and connections from the canvas.',
145
+ {},
146
+ async () => ({
147
+ content: [{ type: 'text', text: await callBlockWerk('clear_canvas', {}) }],
148
+ }),
149
+ );
150
+
151
+ server.tool(
152
+ 'add_block',
153
+ 'Adds a single block to the canvas. Prefer batch_commands when adding multiple blocks.',
154
+ {
155
+ type: z.string().describe('Block type, e.g. Integrator, PIDController, Scope'),
156
+ x: z.number().describe('X position in pixels (0–2000)'),
157
+ y: z.number().describe('Y position in pixels (0–2000)'),
158
+ },
159
+ async ({ type, x, y }) => ({
160
+ content: [{ type: 'text', text: await callBlockWerk('add_block', { type, x, y }) }],
161
+ }),
162
+ );
163
+
164
+ server.tool(
165
+ 'connect_blocks',
166
+ 'Connects an output port of one block to an input port of another.',
167
+ {
168
+ fromBlockId: z.string().describe('Source block ID'),
169
+ fromPortId: z.string().describe('Output port name, e.g. "out"'),
170
+ toBlockId: z.string().describe('Target block ID'),
171
+ toPortId: z.string().describe('Input port name, e.g. "in"'),
172
+ },
173
+ async ({ fromBlockId, fromPortId, toBlockId, toPortId }) => ({
174
+ content: [{ type: 'text', text: await callBlockWerk('connect_blocks', { fromBlockId, fromPortId, toBlockId, toPortId }) }],
175
+ }),
176
+ );
177
+
178
+ server.tool(
179
+ 'update_params',
180
+ 'Updates parameters of a block.',
181
+ {
182
+ blockId: z.string().describe('Block ID to update'),
183
+ params: z.record(z.unknown()).describe('Parameter key-value pairs, e.g. { "value": 1.0 }'),
184
+ },
185
+ async ({ blockId, params }) => ({
186
+ content: [{ type: 'text', text: await callBlockWerk('update_params', { blockId, params }) }],
187
+ }),
188
+ );
189
+
190
+ server.tool(
191
+ 'delete_block',
192
+ 'Deletes a block and all its connections.',
193
+ {
194
+ blockId: z.string().describe('Block ID to delete'),
195
+ },
196
+ async ({ blockId }) => ({
197
+ content: [{ type: 'text', text: await callBlockWerk('delete_block', { blockId }) }],
198
+ }),
199
+ );
200
+
201
+ server.tool(
202
+ 'load_diagram',
203
+ 'Loads a complete BlockWerk project from a JSON string.',
204
+ {
205
+ json: z.string().describe('JSON project data (from exportJson or a saved file)'),
206
+ },
207
+ async ({ json }) => ({
208
+ content: [{ type: 'text', text: await callBlockWerk('load_diagram', { json }) }],
209
+ }),
210
+ );
211
+
212
+ // ── Start ─────────────────────────────────────────────────────────────────────
213
+
214
+ const transport = new StdioServerTransport();
215
+ await server.connect(transport);
216
+ console.error(`[BlockWerk MCP] Connected — session: ${SESSION_ID}, relay: ${RELAY_URL}`);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }