blockwerk-mcp 0.1.1 → 0.2.2

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 ADDED
@@ -0,0 +1,75 @@
1
+ # BlockWerk MCP Server
2
+
3
+ [![npm version](https://img.shields.io/npm/v/blockwerk-mcp.svg)](https://www.npmjs.com/package/blockwerk-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ **BlockWerk MCP Server** bridges external AI clients (like Claude Desktop or Cursor) to a live BlockWerk browser tab. This allows an AI to observe and manipulate your block diagrams in real-time.
7
+
8
+ ---
9
+
10
+ ## 🚀 Features
11
+
12
+ - **Read Canvas**: The AI can see all blocks, connections, and parameters.
13
+ - **Manipulate Canvas**: Add blocks, connect ports, update parameters, and delete blocks.
14
+ - **Simulation Control**: Run simulations, perform frequency sweeps (Bode plots), and analyze system topology.
15
+ - **Layout Engine**: Automatic layout tidying.
16
+
17
+ ## 📦 Installation
18
+
19
+ You can run it directly via `npx`:
20
+
21
+ ```bash
22
+ npx blockwerk-mcp --session <YOUR_SESSION_ID>
23
+ ```
24
+
25
+ Or install it globally:
26
+
27
+ ```bash
28
+ npm install -g blockwerk-mcp
29
+ ```
30
+
31
+ ## 🛠️ Setup
32
+
33
+ ### 1. Get your Session ID
34
+ 1. Open [blockwerk.tech](https://blockwerk.tech).
35
+ 2. Click on the **AI Assistant** panel (right side).
36
+ 3. Select **Connect External AI**.
37
+ 4. Copy the provided **Session ID** (e.g., `bw-a1b2c3d4`).
38
+
39
+ ### 2. Configure your Client
40
+
41
+ #### Claude Desktop
42
+ Add this to your `claude_desktop_config.json`:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "blockwerk": {
48
+ "command": "npx",
49
+ "args": ["-y", "blockwerk-mcp", "--session", "YOUR_SESSION_ID"]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ #### Cursor
56
+ 1. Go to **Settings > Models > MCP**.
57
+ 2. Add a new MCP server:
58
+ - **Name**: BlockWerk
59
+ - **Type**: command
60
+ - **Command**: `npx -y blockwerk-mcp --session YOUR_SESSION_ID`
61
+
62
+ ## 👨‍💻 Development
63
+
64
+ If you want to contribute or run from source:
65
+
66
+ ```bash
67
+ git clone https://github.com/blockwerk/blockwerk
68
+ cd ui/blockwerk/projects/mcp-server
69
+ npm install
70
+ npm run build
71
+ npm run dev -- --session <YOUR_SESSION_ID>
72
+ ```
73
+
74
+ ## 📄 License
75
+ MIT
package/dist/index.js CHANGED
@@ -6,12 +6,23 @@
6
6
  * browser tab via the Netlify relay.
7
7
  *
8
8
  * Usage:
9
- * npx @blockwerk/mcp --session bw-a1b2c3d4
10
- * npx @blockwerk/mcp --session bw-a1b2c3d4 --relay https://blockwerk.tech
9
+ * # Path based (local dev)
10
+ * node projects/mcp-server/dist/index.js --session bw-a1b2c3d4
11
+ * npx tsx projects/mcp-server/src/index.ts --session bw-a1b2c3d4
12
+ *
13
+ * # npm based (after publishing)
14
+ * npx blockwerk-mcp --session bw-a1b2c3d4
11
15
  */
12
16
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
17
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
18
  import { z } from 'zod';
19
+ import crypto from 'node:crypto';
20
+ import { promisify } from 'node:util';
21
+ import { PostHog } from 'posthog-node';
22
+ const posthog = new PostHog(process.env.POSTHOG_API_KEY ?? '', {
23
+ host: process.env.POSTHOG_HOST ?? 'https://eu.i.posthog.com',
24
+ });
25
+ const pbkdf2 = promisify(crypto.pbkdf2);
15
26
  // ── Config ────────────────────────────────────────────────────────────────────
16
27
  const args = process.argv.slice(2);
17
28
  const getArg = (flag) => {
@@ -21,24 +32,79 @@ const getArg = (flag) => {
21
32
  let SESSION_ID = getArg('--session') ?? process.env.BLOCKWERK_SESSION;
22
33
  const RELAY_BASE = (getArg('--relay') ??
23
34
  process.env.BLOCKWERK_RELAY ??
24
- 'https://blockwerk.tech').replace(/\/$/, '');
35
+ 'http://localhost:5173').replace(/\/$/, '');
25
36
  const RELAY_URL = `${RELAY_BASE}/api/mcp-relay`;
26
37
  if (!SESSION_ID) {
27
38
  console.error('[BlockWerk MCP] Warning: No session ID provided at startup.\n' +
28
39
  'Use the "configure_bridge" tool to set the session ID at runtime.');
29
40
  }
41
+ console.error(`[BlockWerk MCP] Connecting to relay: ${RELAY_URL}`);
42
+ // ── Cryptography ─────────────────────────────────────────────────────────────
43
+ const ITERATIONS = 100000;
44
+ const KEY_LEN = 32; // AES-256
45
+ /**
46
+ * Derives an AES-256 key from the session ID using PBKDF2.
47
+ * Uses a per-session salt (first 16 bytes of session ID hashed) to prevent
48
+ * rainbow table attacks against the static salt.
49
+ */
50
+ async function deriveKey(password, saltHex) {
51
+ const salt = saltHex ? Buffer.from(saltHex, 'hex') : crypto.randomBytes(16);
52
+ const key = await pbkdf2(password, salt, ITERATIONS, KEY_LEN, 'sha256');
53
+ return { key, saltHex: salt.toString('hex') };
54
+ }
55
+ async function encryptData(data, sessionId) {
56
+ const { key, saltHex } = await deriveKey(sessionId);
57
+ const iv = crypto.randomBytes(12);
58
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
59
+ const buffer = Buffer.from(JSON.stringify(data));
60
+ const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
61
+ const tag = cipher.getAuthTag();
62
+ // Format: salt:iv:ciphertext+tag (all hex/base64)
63
+ const ivHex = iv.toString('hex');
64
+ const ciphertextBase64 = Buffer.concat([encrypted, tag]).toString('base64');
65
+ return `${saltHex}:${ivHex}:${ciphertextBase64}`;
66
+ }
67
+ async function decryptData(encryptedString, sessionId) {
68
+ try {
69
+ const parts = encryptedString.split(':');
70
+ if (parts.length < 3)
71
+ return null;
72
+ const saltHex = parts[0];
73
+ const ivHex = parts[1];
74
+ const ciphertextBase64 = parts[2];
75
+ const { key } = await deriveKey(sessionId, saltHex);
76
+ const iv = Buffer.from(ivHex, 'hex');
77
+ const combined = Buffer.from(ciphertextBase64, 'base64');
78
+ const ciphertext = combined.subarray(0, combined.length - 16);
79
+ const tag = combined.subarray(combined.length - 16);
80
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
81
+ decipher.setAuthTag(tag);
82
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
83
+ return JSON.parse(decrypted.toString());
84
+ }
85
+ catch (e) {
86
+ console.error('[MCP Crypto] Decryption failed:', e);
87
+ return null;
88
+ }
89
+ }
30
90
  // ── Relay communication ───────────────────────────────────────────────────────
31
91
  const MAX_WAIT_MS = 60000; // total wait before giving up
32
92
  async function callBlockWerk(name, input) {
33
93
  if (!SESSION_ID) {
34
94
  throw new Error('Session ID not configured. Please provide your BlockWerk Session ID first using the "configure_bridge" tool.');
35
95
  }
96
+ posthog.capture({
97
+ distinctId: SESSION_ID,
98
+ event: 'mcp_tool_called',
99
+ properties: { tool: name },
100
+ });
36
101
  const requestId = crypto.randomUUID();
37
- // 1. Push command to relay
102
+ // 1. Push command to relay (ENCRYPTED)
103
+ const encryptedCmd = await encryptData({ requestId, name, input }, SESSION_ID);
38
104
  const pushRes = await fetch(`${RELAY_URL}?session=${SESSION_ID}&type=command`, {
39
105
  method: 'POST',
40
106
  headers: { 'Content-Type': 'application/json' },
41
- body: JSON.stringify({ requestId, name, input }),
107
+ body: JSON.stringify(encryptedCmd), // Relay accepts string as raw body or JSON string
42
108
  });
43
109
  if (!pushRes.ok) {
44
110
  throw new Error(`Relay rejected command: ${pushRes.status} ${await pushRes.text()}`);
@@ -52,8 +118,12 @@ async function callBlockWerk(name, input) {
52
118
  throw new Error(`Relay poll failed: ${pollRes.status}`);
53
119
  }
54
120
  const data = (await pollRes.json());
55
- if (!data.pending) {
56
- return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
121
+ if (!data.pending && data.result) {
122
+ // DECRYPT Result
123
+ const decryptedResult = await decryptData(data.result, SESSION_ID);
124
+ return typeof decryptedResult === 'string'
125
+ ? decryptedResult
126
+ : JSON.stringify(decryptedResult);
57
127
  }
58
128
  // pending: true — relay timed out but result not yet ready, retry immediately
59
129
  }
@@ -62,13 +132,17 @@ async function callBlockWerk(name, input) {
62
132
  // ── MCP server ────────────────────────────────────────────────────────────────
63
133
  const server = new McpServer({
64
134
  name: 'blockwerk',
65
- version: '0.1.0',
135
+ version: '0.2.1',
66
136
  });
67
137
  // ── Tools ─────────────────────────────────────────────────────────────────────
68
138
  server.tool('configure_bridge', 'Connects this AI assistant to a specific BlockWerk browser session.', {
69
139
  sessionId: z.string().describe('The Session ID from the BlockWerk UI (e.g. bw-a1b2c3d4)'),
70
140
  }, async ({ sessionId }) => {
71
141
  SESSION_ID = sessionId;
142
+ posthog.capture({
143
+ distinctId: SESSION_ID,
144
+ event: 'mcp_bridge_configured',
145
+ });
72
146
  return {
73
147
  content: [
74
148
  {
@@ -166,7 +240,77 @@ server.tool('load_diagram', 'Loads a complete BlockWerk project from a JSON stri
166
240
  }, async ({ json }) => ({
167
241
  content: [{ type: 'text', text: await callBlockWerk('load_diagram', { json }) }],
168
242
  }));
243
+ server.tool('get_signal_data', 'Retrieves time-series data (time and values) for a specific block and channel. Channel 0 is usually the primary signal.', {
244
+ blockId: z.string().describe('The ID of the block to inspect (e.g. Scope, UPlotScope)'),
245
+ channel: z.number().int().nonnegative().optional().describe('Channel index, default is 0'),
246
+ }, async ({ blockId, channel }) => ({
247
+ content: [{ type: 'text', text: await callBlockWerk('getSignalData', { blockId, channel }) }],
248
+ }));
249
+ server.tool('calculate_metrics', 'Calculates step response performance metrics (overshoot, rise time, settling time) for a specific block.', {
250
+ blockId: z.string().describe('The ID of the block to analyze'),
251
+ setpoint: z.number().finite().optional().describe('The target setpoint value (default 1.0)'),
252
+ }, async ({ blockId, setpoint }) => ({
253
+ content: [
254
+ { type: 'text', text: await callBlockWerk('calculateMetrics', { blockId, setpoint }) },
255
+ ],
256
+ }));
257
+ server.tool('analyze_topology', 'Analyzes the diagram topology to detect feedback loops, cycles, and connectivity issues.', {}, async () => ({
258
+ content: [{ type: 'text', text: await callBlockWerk('analyzeTopology', {}) }],
259
+ }));
260
+ server.tool('get_engine_status', 'Returns the current status of the simulation engine (Running, Idle, Error), simulation progress, and identifies any algebraic loops.', {}, async () => ({
261
+ content: [{ type: 'text', text: await callBlockWerk('get_engine_status', {}) }],
262
+ }));
263
+ server.tool('get_bode_data', 'Performs a frequency sweep analysis to generate Bode plot data (magnitude and phase) for a system.', {
264
+ inputBlockId: z
265
+ .string()
266
+ .describe('The ID of the block providing the sine excitation (e.g. SineWave)'),
267
+ outputBlockId: z
268
+ .string()
269
+ .describe('The ID of the block to measure (e.g. TransferFunction or Scope)'),
270
+ startFreq: z.number().optional().describe('Start frequency in rad/s (default 0.1)'),
271
+ endFreq: z.number().optional().describe('End frequency in rad/s (default 100)'),
272
+ pointsPerDecade: z
273
+ .number()
274
+ .int()
275
+ .optional()
276
+ .describe('Resolution in points per decade (default 10)'),
277
+ }, async (input) => ({
278
+ content: [{ type: 'text', text: await callBlockWerk('get_bode_data', input) }],
279
+ }));
280
+ server.tool('get_fft_data', 'Calculates the Fast Fourier Transform (FFT) for a specific signal to analyze its frequency spectrum.', {
281
+ blockId: z.string().describe('The ID of the block to analyze'),
282
+ channel: z.number().int().nonnegative().optional().describe('Channel index (default 0)'),
283
+ }, async ({ blockId, channel }) => ({
284
+ content: [{ type: 'text', text: await callBlockWerk('get_fft_data', { blockId, channel }) }],
285
+ }));
286
+ server.tool('undo_last_ai_action', 'Undoes the last AI action, restoring the canvas to its state before the tool was executed.', {}, async () => ({
287
+ content: [{ type: 'text', text: await callBlockWerk('undo_last_ai_action', {}) }],
288
+ }));
289
+ server.tool('redo_last_ai_action', 'Redoes the last undone AI action.', {}, async () => ({
290
+ content: [{ type: 'text', text: await callBlockWerk('redo_last_ai_action', {}) }],
291
+ }));
292
+ server.tool('get_ai_history', 'Returns a summary of recent AI actions with their results and timestamps.', {
293
+ limit: z
294
+ .number()
295
+ .int()
296
+ .positive()
297
+ .optional()
298
+ .describe('Number of recent entries to return (default 10)'),
299
+ }, async ({ limit }) => ({
300
+ content: [{ type: 'text', text: await callBlockWerk('get_ai_history', { limit }) }],
301
+ }));
169
302
  // ── Start ─────────────────────────────────────────────────────────────────────
170
303
  const transport = new StdioServerTransport();
171
304
  await server.connect(transport);
172
305
  console.error(`[BlockWerk MCP] Started — session: ${SESSION_ID || 'PENDING'}, relay: ${RELAY_URL}`);
306
+ posthog.capture({
307
+ distinctId: SESSION_ID ?? 'anonymous',
308
+ event: 'mcp_server_started',
309
+ properties: {
310
+ relay_url: RELAY_BASE,
311
+ has_session: !!SESSION_ID,
312
+ },
313
+ });
314
+ process.on('exit', () => { posthog.shutdown(); });
315
+ process.on('SIGINT', async () => { await posthog.shutdown(); process.exit(0); });
316
+ process.on('SIGTERM', async () => { await posthog.shutdown(); process.exit(0); });
package/package.json CHANGED
@@ -1,26 +1,47 @@
1
- {
2
- "name": "blockwerk-mcp",
3
- "version": "0.1.1",
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
- }
1
+ {
2
+ "name": "blockwerk-mcp",
3
+ "version": "0.2.2",
4
+ "description": "MCP server for BlockWerk — lets Claude Desktop and Cursor control a live BlockWerk browser tab",
5
+ "homepage": "https://blockwerk.tech",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/blockwerk/blockwerk.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/blockwerk/blockwerk/issues"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "blockwerk",
16
+ "block-diagram",
17
+ "simulation",
18
+ "control",
19
+ "ai",
20
+ "claude",
21
+ "cursor"
22
+ ],
23
+ "author": "BlockWerk Team",
24
+ "license": "MIT",
25
+ "type": "module",
26
+ "bin": {
27
+ "blockwerk-mcp": "dist/index.js"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsx src/index.ts",
32
+ "start": "node dist/index.js"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.15.0",
36
+ "posthog-node": "^5.28.11",
37
+ "zod": "^3.25.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "tsx": "^4.19.0",
42
+ "typescript": "~5.9.3"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ }
47
+ }
package/src/index.ts CHANGED
@@ -6,13 +6,26 @@
6
6
  * browser tab via the Netlify relay.
7
7
  *
8
8
  * Usage:
9
- * npx @blockwerk/mcp --session bw-a1b2c3d4
10
- * npx @blockwerk/mcp --session bw-a1b2c3d4 --relay https://blockwerk.tech
9
+ * # Path based (local dev)
10
+ * node projects/mcp-server/dist/index.js --session bw-a1b2c3d4
11
+ * npx tsx projects/mcp-server/src/index.ts --session bw-a1b2c3d4
12
+ *
13
+ * # npm based (after publishing)
14
+ * npx blockwerk-mcp --session bw-a1b2c3d4
11
15
  */
12
16
 
13
17
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
18
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
19
  import { z } from 'zod';
20
+ import crypto from 'node:crypto';
21
+ import { promisify } from 'node:util';
22
+ import { PostHog } from 'posthog-node';
23
+
24
+ const posthog = new PostHog(process.env.POSTHOG_API_KEY ?? '', {
25
+ host: process.env.POSTHOG_HOST ?? 'https://eu.i.posthog.com',
26
+ });
27
+
28
+ const pbkdf2 = promisify(crypto.pbkdf2);
16
29
 
17
30
  // ── Config ────────────────────────────────────────────────────────────────────
18
31
 
@@ -26,7 +39,7 @@ let SESSION_ID = getArg('--session') ?? process.env.BLOCKWERK_SESSION;
26
39
  const RELAY_BASE = (
27
40
  getArg('--relay') ??
28
41
  process.env.BLOCKWERK_RELAY ??
29
- 'https://blockwerk.tech'
42
+ 'http://localhost:5173'
30
43
  ).replace(/\/$/, '');
31
44
  const RELAY_URL = `${RELAY_BASE}/api/mcp-relay`;
32
45
 
@@ -37,6 +50,70 @@ if (!SESSION_ID) {
37
50
  );
38
51
  }
39
52
 
53
+ console.error(`[BlockWerk MCP] Connecting to relay: ${RELAY_URL}`);
54
+
55
+ // ── Cryptography ─────────────────────────────────────────────────────────────
56
+
57
+ const ITERATIONS = 100000;
58
+ const KEY_LEN = 32; // AES-256
59
+
60
+ /**
61
+ * Derives an AES-256 key from the session ID using PBKDF2.
62
+ * Uses a per-session salt (first 16 bytes of session ID hashed) to prevent
63
+ * rainbow table attacks against the static salt.
64
+ */
65
+ async function deriveKey(
66
+ password: string,
67
+ saltHex?: string
68
+ ): Promise<{ key: Buffer; saltHex: string }> {
69
+ const salt = saltHex ? Buffer.from(saltHex, 'hex') : crypto.randomBytes(16);
70
+ const key = await pbkdf2(password, salt, ITERATIONS, KEY_LEN, 'sha256');
71
+ return { key, saltHex: salt.toString('hex') };
72
+ }
73
+
74
+ async function encryptData(data: unknown, sessionId: string): Promise<string> {
75
+ const { key, saltHex } = await deriveKey(sessionId);
76
+ const iv = crypto.randomBytes(12);
77
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
78
+
79
+ const buffer = Buffer.from(JSON.stringify(data));
80
+ const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
81
+ const tag = cipher.getAuthTag();
82
+
83
+ // Format: salt:iv:ciphertext+tag (all hex/base64)
84
+ const ivHex = iv.toString('hex');
85
+ const ciphertextBase64 = Buffer.concat([encrypted, tag]).toString('base64');
86
+
87
+ return `${saltHex}:${ivHex}:${ciphertextBase64}`;
88
+ }
89
+
90
+ async function decryptData(encryptedString: string, sessionId: string): Promise<unknown> {
91
+ try {
92
+ const parts = encryptedString.split(':');
93
+ if (parts.length < 3) return null;
94
+
95
+ const saltHex = parts[0];
96
+ const ivHex = parts[1];
97
+ const ciphertextBase64 = parts[2];
98
+
99
+ const { key } = await deriveKey(sessionId, saltHex);
100
+ const iv = Buffer.from(ivHex, 'hex');
101
+ const combined = Buffer.from(ciphertextBase64, 'base64');
102
+
103
+ const ciphertext = combined.subarray(0, combined.length - 16);
104
+ const tag = combined.subarray(combined.length - 16);
105
+
106
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
107
+ decipher.setAuthTag(tag);
108
+
109
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
110
+ return JSON.parse(decrypted.toString());
111
+ } catch (e) {
112
+ console.error('[MCP Crypto] Decryption failed:', e);
113
+ return null;
114
+ }
115
+ }
116
+
40
117
  // ── Relay communication ───────────────────────────────────────────────────────
41
118
 
42
119
  const MAX_WAIT_MS = 60000; // total wait before giving up
@@ -47,13 +124,20 @@ async function callBlockWerk(name: string, input: Record<string, unknown>): Prom
47
124
  'Session ID not configured. Please provide your BlockWerk Session ID first using the "configure_bridge" tool.'
48
125
  );
49
126
  }
127
+ posthog.capture({
128
+ distinctId: SESSION_ID,
129
+ event: 'mcp_tool_called',
130
+ properties: { tool: name },
131
+ });
50
132
  const requestId = crypto.randomUUID();
51
133
 
52
- // 1. Push command to relay
134
+ // 1. Push command to relay (ENCRYPTED)
135
+ const encryptedCmd = await encryptData({ requestId, name, input }, SESSION_ID);
136
+
53
137
  const pushRes = await fetch(`${RELAY_URL}?session=${SESSION_ID}&type=command`, {
54
138
  method: 'POST',
55
139
  headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify({ requestId, name, input }),
140
+ body: JSON.stringify(encryptedCmd), // Relay accepts string as raw body or JSON string
57
141
  });
58
142
 
59
143
  if (!pushRes.ok) {
@@ -73,10 +157,14 @@ async function callBlockWerk(name: string, input: Record<string, unknown>): Prom
73
157
  throw new Error(`Relay poll failed: ${pollRes.status}`);
74
158
  }
75
159
 
76
- const data = (await pollRes.json()) as { pending?: boolean; result?: unknown };
160
+ const data = (await pollRes.json()) as { pending?: boolean; result?: string };
77
161
 
78
- if (!data.pending) {
79
- return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
162
+ if (!data.pending && data.result) {
163
+ // DECRYPT Result
164
+ const decryptedResult = await decryptData(data.result, SESSION_ID);
165
+ return typeof decryptedResult === 'string'
166
+ ? decryptedResult
167
+ : JSON.stringify(decryptedResult);
80
168
  }
81
169
 
82
170
  // pending: true — relay timed out but result not yet ready, retry immediately
@@ -91,7 +179,7 @@ async function callBlockWerk(name: string, input: Record<string, unknown>): Prom
91
179
 
92
180
  const server = new McpServer({
93
181
  name: 'blockwerk',
94
- version: '0.1.0',
182
+ version: '0.2.1',
95
183
  });
96
184
 
97
185
  // ── Tools ─────────────────────────────────────────────────────────────────────
@@ -104,6 +192,10 @@ server.tool(
104
192
  },
105
193
  async ({ sessionId }) => {
106
194
  SESSION_ID = sessionId;
195
+ posthog.capture({
196
+ distinctId: SESSION_ID,
197
+ event: 'mcp_bridge_configured',
198
+ });
107
199
  return {
108
200
  content: [
109
201
  {
@@ -265,8 +357,129 @@ server.tool(
265
357
  })
266
358
  );
267
359
 
360
+ server.tool(
361
+ 'get_signal_data',
362
+ 'Retrieves time-series data (time and values) for a specific block and channel. Channel 0 is usually the primary signal.',
363
+ {
364
+ blockId: z.string().describe('The ID of the block to inspect (e.g. Scope, UPlotScope)'),
365
+ channel: z.number().int().nonnegative().optional().describe('Channel index, default is 0'),
366
+ },
367
+ async ({ blockId, channel }) => ({
368
+ content: [{ type: 'text', text: await callBlockWerk('getSignalData', { blockId, channel }) }],
369
+ })
370
+ );
371
+
372
+ server.tool(
373
+ 'calculate_metrics',
374
+ 'Calculates step response performance metrics (overshoot, rise time, settling time) for a specific block.',
375
+ {
376
+ blockId: z.string().describe('The ID of the block to analyze'),
377
+ setpoint: z.number().finite().optional().describe('The target setpoint value (default 1.0)'),
378
+ },
379
+ async ({ blockId, setpoint }) => ({
380
+ content: [
381
+ { type: 'text', text: await callBlockWerk('calculateMetrics', { blockId, setpoint }) },
382
+ ],
383
+ })
384
+ );
385
+
386
+ server.tool(
387
+ 'analyze_topology',
388
+ 'Analyzes the diagram topology to detect feedback loops, cycles, and connectivity issues.',
389
+ {},
390
+ async () => ({
391
+ content: [{ type: 'text', text: await callBlockWerk('analyzeTopology', {}) }],
392
+ })
393
+ );
394
+
395
+ server.tool(
396
+ 'get_engine_status',
397
+ 'Returns the current status of the simulation engine (Running, Idle, Error), simulation progress, and identifies any algebraic loops.',
398
+ {},
399
+ async () => ({
400
+ content: [{ type: 'text', text: await callBlockWerk('get_engine_status', {}) }],
401
+ })
402
+ );
403
+
404
+ server.tool(
405
+ 'get_bode_data',
406
+ 'Performs a frequency sweep analysis to generate Bode plot data (magnitude and phase) for a system.',
407
+ {
408
+ inputBlockId: z
409
+ .string()
410
+ .describe('The ID of the block providing the sine excitation (e.g. SineWave)'),
411
+ outputBlockId: z
412
+ .string()
413
+ .describe('The ID of the block to measure (e.g. TransferFunction or Scope)'),
414
+ startFreq: z.number().optional().describe('Start frequency in rad/s (default 0.1)'),
415
+ endFreq: z.number().optional().describe('End frequency in rad/s (default 100)'),
416
+ pointsPerDecade: z
417
+ .number()
418
+ .int()
419
+ .optional()
420
+ .describe('Resolution in points per decade (default 10)'),
421
+ },
422
+ async (input) => ({
423
+ content: [{ type: 'text', text: await callBlockWerk('get_bode_data', input) }],
424
+ })
425
+ );
426
+
427
+ server.tool(
428
+ 'get_fft_data',
429
+ 'Calculates the Fast Fourier Transform (FFT) for a specific signal to analyze its frequency spectrum.',
430
+ {
431
+ blockId: z.string().describe('The ID of the block to analyze'),
432
+ channel: z.number().int().nonnegative().optional().describe('Channel index (default 0)'),
433
+ },
434
+ async ({ blockId, channel }) => ({
435
+ content: [{ type: 'text', text: await callBlockWerk('get_fft_data', { blockId, channel }) }],
436
+ })
437
+ );
438
+
439
+ server.tool(
440
+ 'undo_last_ai_action',
441
+ 'Undoes the last AI action, restoring the canvas to its state before the tool was executed.',
442
+ {},
443
+ async () => ({
444
+ content: [{ type: 'text', text: await callBlockWerk('undo_last_ai_action', {}) }],
445
+ })
446
+ );
447
+
448
+ server.tool('redo_last_ai_action', 'Redoes the last undone AI action.', {}, async () => ({
449
+ content: [{ type: 'text', text: await callBlockWerk('redo_last_ai_action', {}) }],
450
+ }));
451
+
452
+ server.tool(
453
+ 'get_ai_history',
454
+ 'Returns a summary of recent AI actions with their results and timestamps.',
455
+ {
456
+ limit: z
457
+ .number()
458
+ .int()
459
+ .positive()
460
+ .optional()
461
+ .describe('Number of recent entries to return (default 10)'),
462
+ },
463
+ async ({ limit }) => ({
464
+ content: [{ type: 'text', text: await callBlockWerk('get_ai_history', { limit }) }],
465
+ })
466
+ );
467
+
268
468
  // ── Start ─────────────────────────────────────────────────────────────────────
269
469
 
270
470
  const transport = new StdioServerTransport();
271
471
  await server.connect(transport);
272
472
  console.error(`[BlockWerk MCP] Started — session: ${SESSION_ID || 'PENDING'}, relay: ${RELAY_URL}`);
473
+
474
+ posthog.capture({
475
+ distinctId: SESSION_ID ?? 'anonymous',
476
+ event: 'mcp_server_started',
477
+ properties: {
478
+ relay_url: RELAY_BASE,
479
+ has_session: !!SESSION_ID,
480
+ },
481
+ });
482
+
483
+ process.on('exit', () => { posthog.shutdown(); });
484
+ process.on('SIGINT', async () => { await posthog.shutdown(); process.exit(0); });
485
+ process.on('SIGTERM', async () => { await posthog.shutdown(); process.exit(0); });
package/tsconfig.json CHANGED
@@ -1,13 +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
- }
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
+ }