@milaboratories/pl-mcp-server 2.0.0 → 2.0.1

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,101 @@
1
+ # Platforma MCP Server
2
+
3
+ MCP (Model Context Protocol) server that enables AI assistants to interact with the Platforma Desktop App. It exposes tools for project management, block control, UI interaction, screenshot capture, and data queries through the standardized MCP interface.
4
+
5
+ ## Capabilities
6
+
7
+ | Tool group | What it does |
8
+ | ------------------ | ---------------------------------------------------------------- |
9
+ | **Connection** | Connect/disconnect to a running Platforma backend server |
10
+ | **Projects** | List, open, and manage projects |
11
+ | **Blocks** | Add blocks, read block state, configure inputs/arguments |
12
+ | **UI interaction** | Click, type, scroll, and send keyboard events to the desktop app |
13
+ | **Screenshots** | Capture window screenshots (full composited or per-WebContents) |
14
+ | **Data query** | Query PColumn data and result pool contents |
15
+ | **Logs** | Retrieve application logs |
16
+ | **Sandbox** | Execute JavaScript in the app's renderer process |
17
+
18
+ ## How It Works
19
+
20
+ The MCP server runs as a worker process inside the Platforma Desktop App (Electron). It communicates with the main process via IPC and exposes tools over the MCP protocol (stdio transport by default, HTTP also supported).
21
+
22
+ ```
23
+ Claude Code ──MCP──▶ pl-mcp-server (worker) ──IPC──▶ Desktop App (main process)
24
+ ├── Main window
25
+ ├── Block view
26
+ └── Modals
27
+ ```
28
+
29
+ ## Connecting From Claude Code
30
+
31
+ 1. **Start the Platforma Desktop App and enable MCP** — the MCP server feature marked as Alpha for now. To enable it open Settings, scroll down to the bottom, enable checkbox "Enable MCP Server". Then copy MCP Server URL.
32
+
33
+ 2. **Add the MCP server to Claude Code** by creating or editing `.claude/settings.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "pl": {
39
+ "type": "sse",
40
+ "url": "http://localhost:4200/mcp"
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ 3. **Verify the connection** — run `/mcp` in Claude Code to see the `pl` server status. All tools will be available as `mcp__pl__*`.
47
+
48
+ 4. **Connect to the backend** — use `list_connections` to see already saved connections. You could easily reuse them. Or call `connect_to_server` directly with URL, username and password to create new connection.
49
+
50
+ 5. **Open a project** — use `open_project` to start working with a specific project.
51
+
52
+ ## Connecting From Claude Desktop App (Chat)
53
+
54
+ Claude Desktop App uses stdio transport, so it cannot connect to the HTTP MCP endpoint directly. Use `mcp-remote` to bridge the two.
55
+
56
+ 1. **Enable MCP in the Platforma Desktop App** — same as step 1 above (Settings -> "Enable MCP Server"). Copy the MCP Server URL.
57
+
58
+ 2. **Edit Claude Desktop config** — open `~/Library/Application Support/Claude/claude_desktop_config.json` and add the `mcpServers` entry:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "platforma": {
64
+ "command": "npx",
65
+ "args": ["-y", "mcp-remote", "http://127.0.0.1:4200/<your-session-id>/mcp"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ Replace the URL with the actual MCP Server URL from the Platforma Desktop App settings.
72
+
73
+ 3. **Restart Claude Desktop App** — the Platforma tools will appear in your chat sessions. Follow the same workflow: `list_connections` -> `connect_to_server` -> `open_project`.
74
+
75
+ ## Skill
76
+
77
+ There is a `mcp-desktop-testing` skill for Claude in ecosystem overlay. Should be found and used automatically if overlay enabled.
78
+
79
+ ## Coordinate System
80
+
81
+ The desktop app uses Electron with potentially multiple WebContents layers. Screenshots are captured in **device pixels** (2x on Retina displays), while input events use **CSS pixels**.
82
+
83
+ To click on an element visible in a screenshot: divide the screenshot pixel coordinates by the device pixel ratio (typically 2 on macOS Retina).
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ # Build
89
+ pnpm --filter @milaboratories/pl-mcp-server build
90
+
91
+ # Run tests
92
+ pnpm --filter @milaboratories/pl-mcp-server test
93
+
94
+ # Pack for desktop app
95
+ pnpm --filter @milaboratories/pl-mcp-server do-pack
96
+
97
+ # Then reinstall in desktop app
98
+ cd ../../platforma-desktop-app && pnpm install
99
+ ```
100
+
101
+ After changes, rebuild the desktop app's worker package and restart the app to pick up the new server code.
@@ -3,6 +3,7 @@ const require_tokens = require("./tokens.cjs");
3
3
  const require_sandbox = require("./sandbox.cjs");
4
4
  let zod = require("zod");
5
5
  let _platforma_sdk_model = require("@platforma-sdk/model");
6
+ let _milaboratories_pl_errors = require("@milaboratories/pl-errors");
6
7
  //#region src/tools/block-state.ts
7
8
  function registerBlockStateTools(server, ctx) {
8
9
  server.registerTool("get_project_overview", {
@@ -58,10 +59,17 @@ function registerBlockStateTools(server, ctx) {
58
59
  data: zod.z.record(zod.z.unknown()).describe("Block data object")
59
60
  }
60
61
  }, async ({ projectId, blockId, data }) => {
61
- await (await ctx.getOpenedProject(projectId)).mutateBlockStorage(blockId, {
62
- operation: "update-block-data",
63
- value: data
64
- }, ctx.getAuthorMarker());
62
+ const project = await ctx.getOpenedProject(projectId);
63
+ const value = data.args ?? data;
64
+ try {
65
+ await project.mutateBlockStorage(blockId, {
66
+ operation: "update-block-data",
67
+ value
68
+ }, ctx.getAuthorMarker());
69
+ } catch (e) {
70
+ if (e instanceof _milaboratories_pl_errors.ModelAPIVersionMismatchError) await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
71
+ else throw e;
72
+ }
65
73
  return require_types.textResult({ ok: true });
66
74
  });
67
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"block-state.cjs","names":["z","errorResult","textResult","safeEval","summarizeOutputs"],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value: data },\n ctx.getAuthorMarker(),\n );\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;AAQA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAWA,IAAAA,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAOC,cAAAA,YAAY,sCAAsC;AACxE,SAAOC,cAAAA,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAWF,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAWA,IAAAA,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkBA,IAAAA,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,QAAA,GAAA,qBAAA,uBAA6B,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAOE,cAAAA,WALQ,MAAMC,gBAAAA,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAOF,cAAAA,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAOC,cAAAA,WAAW;GAChB;GACA,SAASE,eAAAA,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAWJ,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAMA,IAAAA,EAAE,OAAOA,IAAAA,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;AAEtC,SADgB,MAAM,IAAI,iBAAiB,UAAU,EACvC,mBACZ,SACA;GAAE,WAAW;GAAqB,OAAO;GAAM,EAC/C,IAAI,iBAAiB,CACtB;AACD,SAAOE,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
1
+ {"version":3,"file":"block-state.cjs","names":["z","errorResult","textResult","safeEval","summarizeOutputs","ModelAPIVersionMismatchError"],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { ModelAPIVersionMismatchError } from \"@milaboratories/pl-errors\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n\n // V1 state shape: { args, uiState } — callers may pass either the full state\n // or just the args object. Unwrap once for both V2 and V1 paths.\n const value = data.args ?? data;\n\n // Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).\n try {\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value },\n ctx.getAuthorMarker(),\n );\n } catch (e: unknown) {\n if (e instanceof ModelAPIVersionMismatchError) {\n await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());\n } else {\n throw e;\n }\n }\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAWA,IAAAA,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAOC,cAAAA,YAAY,sCAAsC;AACxE,SAAOC,cAAAA,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAWF,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAWA,IAAAA,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkBA,IAAAA,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,QAAA,GAAA,qBAAA,uBAA6B,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAOE,cAAAA,WALQ,MAAMC,gBAAAA,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAOF,cAAAA,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAOC,cAAAA,WAAW;GAChB;GACA,SAASE,eAAAA,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAWJ,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAMA,IAAAA,EAAE,OAAOA,IAAAA,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;EACtC,MAAM,UAAU,MAAM,IAAI,iBAAiB,UAAU;EAIrD,MAAM,QAAQ,KAAK,QAAQ;AAG3B,MAAI;AACF,SAAM,QAAQ,mBACZ,SACA;IAAE,WAAW;IAAqB;IAAO,EACzC,IAAI,iBAAiB,CACtB;WACM,GAAY;AACnB,OAAI,aAAaK,0BAAAA,6BACf,OAAM,QAAQ,aAAa,SAAS,OAAO,IAAI,iBAAiB,CAAC;OAEjE,OAAM;;AAGV,SAAOH,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
@@ -3,6 +3,7 @@ import { summarizeOutputs } from "./tokens.js";
3
3
  import { safeEval } from "./sandbox.js";
4
4
  import { z } from "zod";
5
5
  import { deriveDataFromStorage } from "@platforma-sdk/model";
6
+ import { ModelAPIVersionMismatchError } from "@milaboratories/pl-errors";
6
7
  //#region src/tools/block-state.ts
7
8
  function registerBlockStateTools(server, ctx) {
8
9
  server.registerTool("get_project_overview", {
@@ -58,10 +59,17 @@ function registerBlockStateTools(server, ctx) {
58
59
  data: z.record(z.unknown()).describe("Block data object")
59
60
  }
60
61
  }, async ({ projectId, blockId, data }) => {
61
- await (await ctx.getOpenedProject(projectId)).mutateBlockStorage(blockId, {
62
- operation: "update-block-data",
63
- value: data
64
- }, ctx.getAuthorMarker());
62
+ const project = await ctx.getOpenedProject(projectId);
63
+ const value = data.args ?? data;
64
+ try {
65
+ await project.mutateBlockStorage(blockId, {
66
+ operation: "update-block-data",
67
+ value
68
+ }, ctx.getAuthorMarker());
69
+ } catch (e) {
70
+ if (e instanceof ModelAPIVersionMismatchError) await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
71
+ else throw e;
72
+ }
65
73
  return textResult({ ok: true });
66
74
  });
67
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"block-state.js","names":[],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value: data },\n ctx.getAuthorMarker(),\n );\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;AAQA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAW,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAO,YAAY,sCAAsC;AACxE,SAAO,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAW,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkB,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,OAAO,sBAAsB,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAO,WALQ,MAAM,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAO,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAO,WAAW;GAChB;GACA,SAAS,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;AAEtC,SADgB,MAAM,IAAI,iBAAiB,UAAU,EACvC,mBACZ,SACA;GAAE,WAAW;GAAqB,OAAO;GAAM,EAC/C,IAAI,iBAAiB,CACtB;AACD,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
1
+ {"version":3,"file":"block-state.js","names":[],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { ModelAPIVersionMismatchError } from \"@milaboratories/pl-errors\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n\n // V1 state shape: { args, uiState } — callers may pass either the full state\n // or just the args object. Unwrap once for both V2 and V1 paths.\n const value = data.args ?? data;\n\n // Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).\n try {\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value },\n ctx.getAuthorMarker(),\n );\n } catch (e: unknown) {\n if (e instanceof ModelAPIVersionMismatchError) {\n await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());\n } else {\n throw e;\n }\n }\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAW,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAO,YAAY,sCAAsC;AACxE,SAAO,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAW,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkB,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,OAAO,sBAAsB,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAO,WALQ,MAAM,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAO,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAO,WAAW;GAChB;GACA,SAAS,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;EACtC,MAAM,UAAU,MAAM,IAAI,iBAAiB,UAAU;EAIrD,MAAM,QAAQ,KAAK,QAAQ;AAG3B,MAAI;AACF,SAAM,QAAQ,mBACZ,SACA;IAAE,WAAW;IAAqB;IAAO,EACzC,IAAI,iBAAiB,CACtB;WACM,GAAY;AACnB,OAAI,aAAa,6BACf,OAAM,QAAQ,aAAa,SAAS,OAAO,IAAI,iBAAiB,CAAC;OAEjE,OAAM;;AAGV,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-mcp-server",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP server for Platforma Desktop",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -28,15 +28,16 @@
28
28
  "@vitest/coverage-istanbul": "^4.1.3",
29
29
  "typescript": "~5.9.3",
30
30
  "vitest": "^4.1.3",
31
- "@milaboratories/build-configs": "2.0.0",
32
31
  "@milaboratories/pl-model-common": "1.31.2",
33
- "@milaboratories/ts-builder": "1.3.1",
34
- "@platforma-sdk/model": "1.65.0",
35
- "@milaboratories/pl-middle-layer": "1.55.9",
36
- "@milaboratories/ts-configs": "1.2.3"
32
+ "@milaboratories/build-configs": "2.0.0",
33
+ "@milaboratories/ts-configs": "1.2.3",
34
+ "@milaboratories/pl-middle-layer": "1.55.11",
35
+ "@milaboratories/ts-builder": "1.3.2",
36
+ "@milaboratories/pl-errors": "1.2.8",
37
+ "@platforma-sdk/model": "1.65.0"
37
38
  },
38
39
  "peerDependencies": {
39
- "@milaboratories/pl-middle-layer": ">=1.55.9",
40
+ "@milaboratories/pl-middle-layer": ">=1.55.11",
40
41
  "@platforma-sdk/model": ">=1.65.0"
41
42
  },
42
43
  "engines": {
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { deriveDataFromStorage } from "@platforma-sdk/model";
3
+ import { ModelAPIVersionMismatchError } from "@milaboratories/pl-errors";
3
4
  import { z } from "zod";
4
5
  import type { ToolContext } from "./types";
5
6
  import { summarizeOutputs } from "./tokens";
@@ -104,11 +105,25 @@ export function registerBlockStateTools(server: McpServer, ctx: ToolContext): vo
104
105
  },
105
106
  async ({ projectId, blockId, data }) => {
106
107
  const project = await ctx.getOpenedProject(projectId);
107
- await project.mutateBlockStorage(
108
- blockId,
109
- { operation: "update-block-data", value: data },
110
- ctx.getAuthorMarker(),
111
- );
108
+
109
+ // V1 state shape: { args, uiState } — callers may pass either the full state
110
+ // or just the args object. Unwrap once for both V2 and V1 paths.
111
+ const value = data.args ?? data;
112
+
113
+ // Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).
114
+ try {
115
+ await project.mutateBlockStorage(
116
+ blockId,
117
+ { operation: "update-block-data", value },
118
+ ctx.getAuthorMarker(),
119
+ );
120
+ } catch (e: unknown) {
121
+ if (e instanceof ModelAPIVersionMismatchError) {
122
+ await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
123
+ } else {
124
+ throw e;
125
+ }
126
+ }
112
127
  return textResult({ ok: true });
113
128
  },
114
129
  );