@milaboratories/pl-mcp-server 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.cjs","names":["StreamableHTTPServerTransport","McpServer"],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /** Execute JavaScript in the renderer and return the result. */\n executeJavaScript?: (code: string) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkFA,IAAa,cAAb,MAAyB;CACvB;CACA;CACA;CACA;CACA;CACA,6BAA8B,IAAI,KAA4C;CAE9E,YAAY,SAA6B;AACvC,OAAK,KAAK,QAAQ,eAAe;AACjC,OAAK,OAAO,QAAQ;AACpB,OAAK,SAAS,QAAQ;AACtB,OAAK,YAAY,QAAQ,aAAa,EAAE;;;CAI1C,eAAe,IAAoC;AACjD,OAAK,KAAK,MAAM;;CAGlB,IAAI,MAAc;AAChB,SAAO,oBAAoB,KAAK,KAAK,GAAG,KAAK,OAAO;;CAGtD,MAAM,QAAuB;AAC3B,MAAI,KAAK,WACP,OAAM,IAAI,MAAM,gCAAgC;EAGlD,MAAM,eAAe,IAAI,KAAK,OAAO;AAErC,OAAK,cAAA,GAAA,UAAA,cAA0B,OAAO,KAAsB,QAAwB;AAClF,OAAI;AAEF,QAAI,IAAI,QAAQ,WAAW,KAAA,EACzB,KAAI;KACF,MAAM,SAAS,IAAI,IAAI,IAAI,QAAQ,OAAO;AAC1C,SAAI,OAAO,aAAa,eAAe,OAAO,aAAa,aAAa;AACtE,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IAAI,KAAK,UAAU,EAAE,OAAO,aAAa,CAAC,CAAC;AAC/C;;YAEI;AACN,SAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,SAAI,IAAI,KAAK,UAAU,EAAE,OAAO,aAAa,CAAC,CAAC;AAC/C;;AAKJ,QAAI,IAAI,QAAQ,cAAc;AAC5B,SAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,SAAI,IAAI,KAAK,UAAU,EAAE,OAAO,aAAa,CAAC,CAAC;AAC/C;;IAIF,MAAM,YAAY,IAAI,QAAQ;AAC9B,QAAI,aAAa,KAAK,WAAW,IAAI,UAAU,EAAE;AAC/C,WAAM,KAAK,WAAW,IAAI,UAAU,CAAE,cAAc,KAAK,IAAI;AAC7D;;IAIF,MAAM,YAAY,IAAIA,mDAAAA,8BAA8B,EAClD,2BAAA,GAAA,YAAA,aAAsC,EACvC,CAAC;AACF,cAAU,gBAAgB;KACxB,MAAM,MAAM,UAAU;AACtB,SAAI,IAAK,MAAK,WAAW,OAAO,IAAI;;AAItC,UADe,KAAK,iBAAiB,CACxB,QAAQ,UAAU;AAC/B,UAAM,UAAU,cAAc,KAAK,IAAI;IAEvC,MAAM,MAAM,UAAU;AACtB,QAAI,IAAK,MAAK,WAAW,IAAI,KAAK,UAAU;WACtC;AACN,QAAI,CAAC,IAAI,aAAa;AACpB,SAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,SAAI,IAAI,KAAK,UAAU,EAAE,OAAO,yBAAyB,CAAC,CAAC;;;IAG/D;EAEF,MAAM,aAAa;EACnB,MAAM,iBAAiB,KAAK,WAAW,UAAU,UAAU,CAAC;AAI5D,OAAK,IAAI,UAAU,GAAG,UAAU,YAAY,WAAW;GACrD,MAAM,SAAS,KAAK;AACpB,OAAI;AACF,UAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,YAAO,OAAO,KAAK,MAAM,mBAAmB,SAAS,CAAC;AACtD,YAAO,KAAK,SAAS,OAAO;MAC5B;IAEF,MAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,QAAQ,OAAO,SAAS,SAC1B,MAAK,OAAO,KAAK;AAEnB;YACO,KAAc;AACrB,QACE,eAAe,SACf,UAAU,OACV,IAAI,SAAS,gBACb,UAAU,aAAa,GACvB;AACA,YAAO,oBAAoB;AAC3B,UAAK,cAAA,GAAA,UAAA,cAA0B,eAAe;AAC9C,UAAK;AACL;;AAEF,UAAM;;;;CAKZ,MAAM,OAAsB;AAC1B,OAAK,MAAM,aAAa,KAAK,WAAW,QAAQ,CAC9C,OAAM,UAAU,OAAO;AAEzB,OAAK,WAAW,OAAO;EAEvB,MAAM,SAAS,KAAK;AACpB,MAAI,QAAQ;AACV,SAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,WAAO,OAAO,QAAS,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACtD;AACF,QAAK,aAAa,KAAA;;;CAItB,kBAAqC;EACnC,MAAM,aAAA,GAAA,YAAA,aAAwB,CAAC,MAAM,GAAG,EAAE;EAC1C,MAAM,SAAS,IAAIC,wCAAAA,UAAU;GAAE,MAAM;GAAM,SAAS;GAAS,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AAC/F,OAAK,cAAc,QAAQ,UAAU;AACrC,SAAO;;CAGT,cAAsB,QAAmB,WAAyB;EAChE,MAAM,WAAW,OAAO;EACxB,IAAI,eAAe;EACnB,MAAM,MAAmB;GACvB,aAAa,KAAK;GAClB,iBAAiB,KAAK,WAAW;GACjC,iBAAiB,OAAO,KAAK,eAAe,GAAG;GAC/C,mBAAmB,OAAO,KAAK,iBAAiB,GAAG;GACnD,WAAW,KAAK;GAChB,wBAAwB;IAAE;IAAU,cAAc,EAAE;IAAc;GACnE;AACD,eAAA,iBAAiB,QAAQ,IAAI;AAC7B,qBAAA,wBAAwB,QAAQ,IAAI;AACpC,mBAAA,qBAAqB,QAAQ,IAAI;AACjC,iBAAA,mBAAmB,QAAQ,IAAI;AAC/B,sBAAA,wBAAwB,QAAQ,IAAI;AACpC,gBAAA,mBAAmB,QAAQ,IAAI;AAC/B,eAAA,iBAAiB,QAAQ,IAAI;AAC7B,qBAAA,uBAAuB,QAAQ,IAAI;AACnC,qBAAA,uBAAuB,QAAQ,IAAI;AACnC,yBAAA,2BAA2B,QAAQ,IAAI;;;CAIzC,YAAiC;AAC/B,MAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,0DAA0D;AACxF,SAAO,KAAK;;;CAId,MAAc,eAAe,WAAmB;EAC9C,MAAM,KAAK,KAAK,WAAW;AAC3B,QAAM,GAAG,YAAY,cAAc;EAEnC,MAAM,SADW,MAAM,GAAG,YAAY,kBAAkB,EACjC,MAAM,OAAA,GAAA,gCAAA,oBAAyB,EAAE,IAAI,KAAK,UAAU;AAC3E,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,WAAW,UAAU,YAAY;AAC7D,SAAO;;;CAIT,MAAc,iBAAiB,WAAmB;EAChD,MAAM,KAAK,KAAK,WAAW;EAC3B,MAAM,QAAQ,MAAM,KAAK,eAAe,UAAU;AAClD,SAAO,GAAG,iBAAiB,MAAM,IAAI"}
1
+ {"version":3,"file":"server.cjs","names":["StreamableHTTPServerTransport","McpServer"],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /**\n * Execute JavaScript in a renderer and return the result.\n * With no `target`, runs in the topmost webContents (usually the main app).\n * With `target`, runs in the specified block's webview (where `window.platforma` is\n * exposed) — the block must already be loaded (e.g. via `select_block`).\n */\n executeJavaScript?: (\n code: string,\n target?: { projectId: string; blockId: string },\n ) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -11,8 +11,16 @@ interface PlMcpServerCallbacks {
11
11
  captureScreenshot?: () => Promise<string>;
12
12
  /** Send an input event to the application window. */
13
13
  sendInputEvent?: (event: unknown) => Promise<void>;
14
- /** Execute JavaScript in the renderer and return the result. */
15
- executeJavaScript?: (code: string) => Promise<unknown>;
14
+ /**
15
+ * Execute JavaScript in a renderer and return the result.
16
+ * With no `target`, runs in the topmost webContents (usually the main app).
17
+ * With `target`, runs in the specified block's webview (where `window.platforma` is
18
+ * exposed) — the block must already be loaded (e.g. via `select_block`).
19
+ */
20
+ executeJavaScript?: (code: string, target?: {
21
+ projectId: string;
22
+ blockId: string;
23
+ }) => Promise<unknown>;
16
24
  /** List available blocks from all configured registries. */
17
25
  listAvailableBlocks?: (query?: string) => Promise<unknown[]>;
18
26
  /** Navigate the desktop UI to show a specific block. */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;UAkBiB,oBAAA;EACf,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EACjD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EAHA;EAKjD,iBAAA,SAA0B,OAAA;EAHsB;EAKhD,cAAA,IAAkB,KAAA,cAAmB,OAAA;EAFX;EAI1B,iBAAA,IAAqB,IAAA,aAAiB,OAAA;EAAA;EAEtC,mBAAA,IAAuB,KAAA,cAAmB,OAAA;EAEY;EAAtD,WAAA,IAAe,SAAA,UAAmB,OAAA,aAAoB,OAAA;EAItB;EAFhC,UAAA,IAAc,KAAA,UAAe,MAAA,cAAoB,OAAA;EAQ5C;EANL,eAAA,SAAwB,OAAA,CAAQ,gBAAA;EAeb;EAbnB,eAAA,IACE,IAAA,UACA,KAAA,UACA,QAAA,cACG,OAAA;IAAU,MAAA;IAAgB,OAAA;EAAA;EAvBX;EAyBpB,mBAAA,SAA4B,OAAA;IAC1B,SAAA;IACA,IAAA;IACA,IAAA;IACA,KAAA;EAAA;EA3B8C;EA8BhD,UAAA,SAAmB,OAAA;EA7BC;EA+BpB,YAAA,IACE,WAAA,UACA,YAAA,UACA,IAAA,UACA,OAAA,aACG,OAAA;AAAA;AAAA,UAGU,gBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,aAAA;AAAA;;KAIU,SAAA,GAAY,OAAA;AAAA,UAEP,kBAAA;EAzC2B;EA2C1C,WAAA,GAAc,WAAA;EAzCC;EA2Cf,IAAA;EA3CsD;EA6CtD,MAAA,EAAQ,SAAA;EA3CM;EA6Cd,SAAA,GAAY,oBAAA;AAAA;AAAA,cAGD,WAAA;EAAA,QACH,EAAA;EAAA,QACA,IAAA;EAAA,iBACS,MAAA;EAAA,iBACA,SAAA;EAAA,QACT,UAAA;EAAA,iBACS,UAAA;cAEL,OAAA,EAAS,kBAAA;EAhDN;EAwDf,cAAA,CAAe,EAAA,EAAI,WAAA;EAAA,IAIf,GAAA,CAAA;EAIE,KAAA,CAAA,GAAS,OAAA;EAiGT,IAAA,CAAA,GAAQ,OAAA;EAAA,QAeN,eAAA;EAAA,QAOA,aAAA;EAjLN;EAAA,QAyMM,SAAA;EAtMW;EAAA,QA4ML,cAAA;EAzMZ;EAAA,QAmNY,gBAAA;AAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;UAkBiB,oBAAA;EACf,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EACjD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EAHA;EAKjD,iBAAA,SAA0B,OAAA;EAHsB;EAKhD,cAAA,IAAkB,KAAA,cAAmB,OAAA;EAFX;;;;;;EAS1B,iBAAA,IACE,IAAA,UACA,MAAA;IAAW,SAAA;IAAmB,OAAA;EAAA,MAC3B,OAAA;EAuBc;EArBnB,mBAAA,IAAuB,KAAA,cAAmB,OAAA;EA4B9B;EA1BZ,WAAA,IAAe,SAAA,UAAmB,OAAA,aAAoB,OAAA;EArBtD;EAuBA,UAAA,IAAc,KAAA,UAAe,MAAA,cAAoB,OAAA;EAvBA;EAyBjD,eAAA,SAAwB,OAAA,CAAQ,gBAAA;EAxBb;EA0BnB,eAAA,IACE,IAAA,UACA,KAAA,UACA,QAAA,cACG,OAAA;IAAU,MAAA;IAAgB,OAAA;EAAA;EA5B/B;EA8BA,mBAAA,SAA4B,OAAA;IAC1B,SAAA;IACA,IAAA;IACA,IAAA;IACA,KAAA;EAAA;EA9BmC;EAiCrC,UAAA,SAAmB,OAAA;EAzBjB;EA2BF,YAAA,IACE,WAAA,UACA,YAAA,UACA,IAAA,UACA,OAAA,aACG,OAAA;AAAA;AAAA,UAGU,gBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,aAAA;AAAA;;KAIU,SAAA,GAAY,OAAA;AAAA,UAEP,kBAAA;EArCf;EAuCA,WAAA,GAAc,WAAA;EAvCe;EAyC7B,IAAA;EAvCA;EAyCA,MAAA,EAAQ,SAAA;EAzCwB;EA2ChC,SAAA,GAAY,oBAAA;AAAA;AAAA,cAGD,WAAA;EAAA,QACH,EAAA;EAAA,QACA,IAAA;EAAA,iBACS,MAAA;EAAA,iBACA,SAAA;EAAA,QACT,UAAA;EAAA,iBACS,UAAA;cAEL,OAAA,EAAS,kBAAA;EA5CnB;EAoDF,cAAA,CAAe,EAAA,EAAI,WAAA;EAAA,IAIf,GAAA,CAAA;EAIE,KAAA,CAAA,GAAS,OAAA;EAiGT,IAAA,CAAA,GAAQ,OAAA;EAAA,QAeN,eAAA;EAAA,QAOA,aAAA;EA1KN;EAAA,QAkMM,SAAA;EAhMN;EAAA,QAsMY,cAAA;EArMF;EAAA,QA+ME,gBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /** Execute JavaScript in the renderer and return the result. */\n executeJavaScript?: (code: string) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /**\n * Execute JavaScript in a renderer and return the result.\n * With no `target`, runs in the topmost webContents (usually the main app).\n * With `target`, runs in the specified block's webview (where `window.platforma` is\n * exposed) — the block must already be loaded (e.g. via `select_block`).\n */\n executeJavaScript?: (\n code: string,\n target?: { projectId: string; blockId: string },\n ) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":"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"}
@@ -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"}
@@ -104,11 +104,20 @@ function registerUIInteractionTools(server, ctx) {
104
104
  return require_types.textResult({ ok: true });
105
105
  });
106
106
  server.registerTool("execute_js", {
107
- description: "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
108
- inputSchema: { code: zod.z.string().describe("JavaScript code to execute") }
109
- }, async ({ code }) => {
107
+ description: "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
108
+ inputSchema: {
109
+ code: zod.z.string().describe("JavaScript code to execute"),
110
+ projectId: zod.z.string().optional().describe("Target project ID. Must be paired with blockId."),
111
+ blockId: zod.z.string().optional().describe("Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).")
112
+ }
113
+ }, async ({ code, projectId, blockId }) => {
110
114
  if (!ctx.callbacks.executeJavaScript) return require_types.errorResult("JS execution is not available.", "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log");
111
- return require_types.textResult(await ctx.callbacks.executeJavaScript(code));
115
+ if (projectId === void 0 !== (blockId === void 0)) return require_types.errorResult("projectId and blockId must be provided together.", "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.");
116
+ const target = projectId !== void 0 && blockId !== void 0 ? {
117
+ projectId,
118
+ blockId
119
+ } : void 0;
120
+ return require_types.textResult(await ctx.callbacks.executeJavaScript(code, target));
112
121
  });
113
122
  }
114
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ui-interaction.cjs","names":["z","errorResult","textResult"],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n },\n },\n async ({ code }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const result = await ctx.callbacks.executeJavaScript(code);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ui-interaction.cjs","names":["z","errorResult","textResult"],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n projectId: z\n .string()\n .optional()\n .describe(\"Target project ID. Must be paired with blockId.\"),\n blockId: z\n .string()\n .optional()\n .describe(\n \"Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).\",\n ),\n },\n },\n async ({ code, projectId, blockId }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n if ((projectId === undefined) !== (blockId === undefined)) {\n return errorResult(\n \"projectId and blockId must be provided together.\",\n \"Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.\",\n );\n }\n const target =\n projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;\n const result = await ctx.callbacks.executeJavaScript(code, target);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
@@ -104,11 +104,20 @@ function registerUIInteractionTools(server, ctx) {
104
104
  return textResult({ ok: true });
105
105
  });
106
106
  server.registerTool("execute_js", {
107
- description: "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
108
- inputSchema: { code: z.string().describe("JavaScript code to execute") }
109
- }, async ({ code }) => {
107
+ description: "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
108
+ inputSchema: {
109
+ code: z.string().describe("JavaScript code to execute"),
110
+ projectId: z.string().optional().describe("Target project ID. Must be paired with blockId."),
111
+ blockId: z.string().optional().describe("Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).")
112
+ }
113
+ }, async ({ code, projectId, blockId }) => {
110
114
  if (!ctx.callbacks.executeJavaScript) return errorResult("JS execution is not available.", "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log");
111
- return textResult(await ctx.callbacks.executeJavaScript(code));
115
+ if (projectId === void 0 !== (blockId === void 0)) return errorResult("projectId and blockId must be provided together.", "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.");
116
+ const target = projectId !== void 0 && blockId !== void 0 ? {
117
+ projectId,
118
+ blockId
119
+ } : void 0;
120
+ return textResult(await ctx.callbacks.executeJavaScript(code, target));
112
121
  });
113
122
  }
114
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ui-interaction.js","names":[],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n },\n },\n async ({ code }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const result = await ctx.callbacks.executeJavaScript(code);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ui-interaction.js","names":[],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n projectId: z\n .string()\n .optional()\n .describe(\"Target project ID. Must be paired with blockId.\"),\n blockId: z\n .string()\n .optional()\n .describe(\n \"Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).\",\n ),\n },\n },\n async ({ code, projectId, blockId }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n if ((projectId === undefined) !== (blockId === undefined)) {\n return errorResult(\n \"projectId and blockId must be provided together.\",\n \"Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.\",\n );\n }\n const target =\n projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;\n const result = await ctx.callbacks.executeJavaScript(code, target);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
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.1.0",
4
4
  "description": "MCP server for Platforma Desktop",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -29,15 +29,16 @@
29
29
  "typescript": "~5.9.3",
30
30
  "vitest": "^4.1.3",
31
31
  "@milaboratories/build-configs": "2.0.0",
32
+ "@milaboratories/pl-errors": "1.2.8",
33
+ "@milaboratories/ts-configs": "1.2.3",
32
34
  "@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"
35
+ "@milaboratories/ts-builder": "1.3.2",
36
+ "@milaboratories/pl-middle-layer": "1.55.13",
37
+ "@platforma-sdk/model": "1.65.4"
37
38
  },
38
39
  "peerDependencies": {
39
- "@milaboratories/pl-middle-layer": ">=1.55.9",
40
- "@platforma-sdk/model": ">=1.65.0"
40
+ "@milaboratories/pl-middle-layer": ">=1.55.13",
41
+ "@platforma-sdk/model": ">=1.65.4"
41
42
  },
42
43
  "engines": {
43
44
  "node": ">=22.0.0"
package/src/server.ts CHANGED
@@ -25,8 +25,16 @@ export interface PlMcpServerCallbacks {
25
25
  captureScreenshot?: () => Promise<string>;
26
26
  /** Send an input event to the application window. */
27
27
  sendInputEvent?: (event: unknown) => Promise<void>;
28
- /** Execute JavaScript in the renderer and return the result. */
29
- executeJavaScript?: (code: string) => Promise<unknown>;
28
+ /**
29
+ * Execute JavaScript in a renderer and return the result.
30
+ * With no `target`, runs in the topmost webContents (usually the main app).
31
+ * With `target`, runs in the specified block's webview (where `window.platforma` is
32
+ * exposed) — the block must already be loaded (e.g. via `select_block`).
33
+ */
34
+ executeJavaScript?: (
35
+ code: string,
36
+ target?: { projectId: string; blockId: string },
37
+ ) => Promise<unknown>;
30
38
  /** List available blocks from all configured registries. */
31
39
  listAvailableBlocks?: (query?: string) => Promise<unknown[]>;
32
40
  /** Navigate the desktop UI to show a specific block. */
@@ -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
  );
@@ -137,19 +137,37 @@ export function registerUIInteractionTools(server: McpServer, ctx: ToolContext):
137
137
  "execute_js",
138
138
  {
139
139
  description:
140
- "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
140
+ "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
141
141
  inputSchema: {
142
142
  code: z.string().describe("JavaScript code to execute"),
143
+ projectId: z
144
+ .string()
145
+ .optional()
146
+ .describe("Target project ID. Must be paired with blockId."),
147
+ blockId: z
148
+ .string()
149
+ .optional()
150
+ .describe(
151
+ "Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).",
152
+ ),
143
153
  },
144
154
  },
145
- async ({ code }) => {
155
+ async ({ code, projectId, blockId }) => {
146
156
  if (!ctx.callbacks.executeJavaScript) {
147
157
  return errorResult(
148
158
  "JS execution is not available.",
149
159
  "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
150
160
  );
151
161
  }
152
- const result = await ctx.callbacks.executeJavaScript(code);
162
+ if ((projectId === undefined) !== (blockId === undefined)) {
163
+ return errorResult(
164
+ "projectId and blockId must be provided together.",
165
+ "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.",
166
+ );
167
+ }
168
+ const target =
169
+ projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;
170
+ const result = await ctx.callbacks.executeJavaScript(code, target);
153
171
  return textResult(result);
154
172
  },
155
173
  );