@milaboratories/pl-mcp-server 1.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 +101 -0
- package/dist/tools/block-state.cjs +12 -4
- package/dist/tools/block-state.cjs.map +1 -1
- package/dist/tools/block-state.js +12 -4
- package/dist/tools/block-state.js.map +1 -1
- package/package.json +9 -8
- package/src/tools/block-state.ts +20 -5
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "MCP server for Platforma Desktop",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -28,16 +28,17 @@
|
|
|
28
28
|
"@vitest/coverage-istanbul": "^4.1.3",
|
|
29
29
|
"typescript": "~5.9.3",
|
|
30
30
|
"vitest": "^4.1.3",
|
|
31
|
-
"@milaboratories/pl-middle-layer": "1.55.8",
|
|
32
|
-
"@milaboratories/build-configs": "2.0.0",
|
|
33
|
-
"@milaboratories/ts-builder": "1.3.1",
|
|
34
31
|
"@milaboratories/pl-model-common": "1.31.2",
|
|
35
|
-
"@
|
|
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.
|
|
40
|
-
"@platforma-sdk/model": ">=1.
|
|
40
|
+
"@milaboratories/pl-middle-layer": ">=1.55.11",
|
|
41
|
+
"@platforma-sdk/model": ">=1.65.0"
|
|
41
42
|
},
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=22.0.0"
|
package/src/tools/block-state.ts
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
);
|