@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 +101 -0
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +10 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/block-state.cjs +12 -4
- package/dist/tools/block-state.cjs.map +1 -1
- package/dist/tools/block-state.js +12 -4
- package/dist/tools/block-state.js.map +1 -1
- package/dist/tools/ui-interaction.cjs +13 -4
- package/dist/tools/ui-interaction.cjs.map +1 -1
- package/dist/tools/ui-interaction.js +13 -4
- package/dist/tools/ui-interaction.js.map +1 -1
- package/package.json +8 -7
- package/src/server.ts +10 -2
- package/src/tools/block-state.ts +20 -5
- package/src/tools/ui-interaction.ts +21 -3
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.
|
package/dist/server.cjs.map
CHANGED
|
@@ -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":"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"}
|
|
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
|
-
/**
|
|
15
|
-
|
|
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. */
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/server.js.map
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const project = await ctx.getOpenedProject(projectId);
|
|
63
|
+
const value = data.args ?? data;
|
|
64
|
+
try {
|
|
65
|
+
await project.mutateBlockStorage(blockId, {
|
|
66
|
+
operation: "update-block-data",
|
|
67
|
+
value
|
|
68
|
+
}, ctx.getAuthorMarker());
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (e instanceof _milaboratories_pl_errors.ModelAPIVersionMismatchError) await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
|
|
71
|
+
else throw e;
|
|
72
|
+
}
|
|
65
73
|
return require_types.textResult({ ok: true });
|
|
66
74
|
});
|
|
67
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-state.cjs","names":["z","errorResult","textResult","safeEval","summarizeOutputs"],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n await project.mutateBlockStorage(\n
|
|
1
|
+
{"version":3,"file":"block-state.cjs","names":["z","errorResult","textResult","safeEval","summarizeOutputs","ModelAPIVersionMismatchError"],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { ModelAPIVersionMismatchError } from \"@milaboratories/pl-errors\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n\n // V1 state shape: { args, uiState } — callers may pass either the full state\n // or just the args object. Unwrap once for both V2 and V1 paths.\n const value = data.args ?? data;\n\n // Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).\n try {\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value },\n ctx.getAuthorMarker(),\n );\n } catch (e: unknown) {\n if (e instanceof ModelAPIVersionMismatchError) {\n await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());\n } else {\n throw e;\n }\n }\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAWA,IAAAA,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAOC,cAAAA,YAAY,sCAAsC;AACxE,SAAOC,cAAAA,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAWF,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAWA,IAAAA,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkBA,IAAAA,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,QAAA,GAAA,qBAAA,uBAA6B,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAOE,cAAAA,WALQ,MAAMC,gBAAAA,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAOF,cAAAA,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAOC,cAAAA,WAAW;GAChB;GACA,SAASE,eAAAA,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAWJ,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAMA,IAAAA,EAAE,OAAOA,IAAAA,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;EACtC,MAAM,UAAU,MAAM,IAAI,iBAAiB,UAAU;EAIrD,MAAM,QAAQ,KAAK,QAAQ;AAG3B,MAAI;AACF,SAAM,QAAQ,mBACZ,SACA;IAAE,WAAW;IAAqB;IAAO,EACzC,IAAI,iBAAiB,CACtB;WACM,GAAY;AACnB,OAAI,aAAaK,0BAAAA,6BACf,OAAM,QAAQ,aAAa,SAAS,OAAO,IAAI,iBAAiB,CAAC;OAEjE,OAAM;;AAGV,SAAOH,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
|
|
@@ -3,6 +3,7 @@ import { summarizeOutputs } from "./tokens.js";
|
|
|
3
3
|
import { safeEval } from "./sandbox.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { deriveDataFromStorage } from "@platforma-sdk/model";
|
|
6
|
+
import { ModelAPIVersionMismatchError } from "@milaboratories/pl-errors";
|
|
6
7
|
//#region src/tools/block-state.ts
|
|
7
8
|
function registerBlockStateTools(server, ctx) {
|
|
8
9
|
server.registerTool("get_project_overview", {
|
|
@@ -58,10 +59,17 @@ function registerBlockStateTools(server, ctx) {
|
|
|
58
59
|
data: z.record(z.unknown()).describe("Block data object")
|
|
59
60
|
}
|
|
60
61
|
}, async ({ projectId, blockId, data }) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const project = await ctx.getOpenedProject(projectId);
|
|
63
|
+
const value = data.args ?? data;
|
|
64
|
+
try {
|
|
65
|
+
await project.mutateBlockStorage(blockId, {
|
|
66
|
+
operation: "update-block-data",
|
|
67
|
+
value
|
|
68
|
+
}, ctx.getAuthorMarker());
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (e instanceof ModelAPIVersionMismatchError) await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
|
|
71
|
+
else throw e;
|
|
72
|
+
}
|
|
65
73
|
return textResult({ ok: true });
|
|
66
74
|
});
|
|
67
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-state.js","names":[],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n await project.mutateBlockStorage(\n
|
|
1
|
+
{"version":3,"file":"block-state.js","names":[],"sources":["../../src/tools/block-state.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { deriveDataFromStorage } from \"@platforma-sdk/model\";\nimport { ModelAPIVersionMismatchError } from \"@milaboratories/pl-errors\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { summarizeOutputs } from \"./tokens\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_project_overview\",\n {\n description:\n \"Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID (must be opened)\"),\n },\n },\n async ({ projectId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const overview = await project.overview.getValue();\n if (!overview) return errorResult(\"Project overview not available yet.\");\n return textResult({\n label: overview.meta.label,\n blocks: overview.blocks.map((b) => ({\n id: b.id,\n title: b.title ?? b.label,\n calculationStatus: b.calculationStatus,\n canRun: b.canRun,\n stale: b.stale,\n inputsValid: b.inputsValid,\n outputErrors: b.outputErrors,\n upstreams: b.upstreams,\n downstreams: b.downstreams,\n })),\n });\n },\n );\n\n server.registerTool(\n \"get_block_state\",\n {\n description:\n \"Get block state. Returns block args (data) and a concise output summary with token estimates by default. \" +\n \"Use `transform` to extract specific data server-side without loading full outputs into context.\\n\\n\" +\n \"Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\\n\\n\" +\n \"Transform examples:\\n\" +\n \"- `outputs.logs?.value` — get one specific output value\\n\" +\n \"- `data` — get only block args\\n\" +\n \"- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against full block state. \" +\n \"Available variables: `data` (block args), `outputs` (raw outputs object). \" +\n \"Omit for default concise summary.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ projectId, blockId, transform, transformTimeout }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n const data = deriveDataFromStorage(state.blockStorage);\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n { data, outputs: state.outputs },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: data, outputs.\",\n );\n }\n }\n return textResult({\n data,\n outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),\n });\n },\n );\n\n server.registerTool(\n \"set_block_data\",\n {\n description: \"Set the user-facing data of a block (triggers args derivation and staging)\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n data: z.record(z.unknown()).describe(\"Block data object\"),\n },\n },\n async ({ projectId, blockId, data }) => {\n const project = await ctx.getOpenedProject(projectId);\n\n // V1 state shape: { args, uiState } — callers may pass either the full state\n // or just the args object. Unwrap once for both V2 and V1 paths.\n const value = data.args ?? data;\n\n // Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).\n try {\n await project.mutateBlockStorage(\n blockId,\n { operation: \"update-block-data\", value },\n ctx.getAuthorMarker(),\n );\n } catch (e: unknown) {\n if (e instanceof ModelAPIVersionMismatchError) {\n await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());\n } else {\n throw e;\n }\n }\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,wBACA;EACE,aACE;EACF,aAAa,EACX,WAAW,EAAE,QAAQ,CAAC,SAAS,8BAA8B,EAC9D;EACF,EACD,OAAO,EAAE,gBAAgB;EAEvB,MAAM,WAAW,OADD,MAAM,IAAI,iBAAiB,UAAU,EACtB,SAAS,UAAU;AAClD,MAAI,CAAC,SAAU,QAAO,YAAY,sCAAsC;AACxE,SAAO,WAAW;GAChB,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,OAAO,KAAK,OAAO;IAClC,IAAI,EAAE;IACN,OAAO,EAAE,SAAS,EAAE;IACpB,mBAAmB,EAAE;IACrB,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,aAAa,EAAE;IACf,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,aAAa,EAAE;IAChB,EAAE;GACJ,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EAOF,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,WAAW,EACR,QAAQ,CACR,UAAU,CACV,SACC,4KAGD;GACH,kBAAkB,EACf,QAAQ,CACR,UAAU,CACV,QAAQ,IAAK,CACb,SAAS,yDAAyD;GACtE;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW,uBAAuB;EAE7D,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;EAC7D,MAAM,OAAO,sBAAsB,MAAM,aAAa;AACtD,MAAI,UACF,KAAI;AAMF,UAAO,WALQ,MAAM,SACnB,WACA;IAAE;IAAM,SAAS,MAAM;IAAS,EAChC,iBACD,CACwB;WAClB,GAAY;AACnB,UAAO,YACL,qBAAqB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,IAC/D,uEACD;;AAGL,SAAO,WAAW;GAChB;GACA,SAAS,iBAAiB,MAAM,QAA+C;GAChF,CAAC;GAEL;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,SAAS,oBAAoB;GAC1D;EACF,EACD,OAAO,EAAE,WAAW,SAAS,WAAW;EACtC,MAAM,UAAU,MAAM,IAAI,iBAAiB,UAAU;EAIrD,MAAM,QAAQ,KAAK,QAAQ;AAG3B,MAAI;AACF,SAAM,QAAQ,mBACZ,SACA;IAAE,WAAW;IAAqB;IAAO,EACzC,IAAI,iBAAiB,CACtB;WACM,GAAY;AACnB,OAAI,aAAa,6BACf,OAAM,QAAQ,aAAa,SAAS,OAAO,IAAI,iBAAiB,CAAC;OAEjE,OAAM;;AAGV,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
|
|
@@ -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
|
|
108
|
-
inputSchema: {
|
|
109
|
-
|
|
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.
|
|
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
|
|
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
|
|
108
|
-
inputSchema: {
|
|
109
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
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.
|
|
40
|
-
"@platforma-sdk/model": ">=1.65.
|
|
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
|
-
/**
|
|
29
|
-
|
|
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. */
|
package/src/tools/block-state.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { deriveDataFromStorage } from "@platforma-sdk/model";
|
|
3
|
+
import { ModelAPIVersionMismatchError } from "@milaboratories/pl-errors";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import type { ToolContext } from "./types";
|
|
5
6
|
import { summarizeOutputs } from "./tokens";
|
|
@@ -104,11 +105,25 @@ export function registerBlockStateTools(server: McpServer, ctx: ToolContext): vo
|
|
|
104
105
|
},
|
|
105
106
|
async ({ projectId, blockId, data }) => {
|
|
106
107
|
const project = await ctx.getOpenedProject(projectId);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
|
|
109
|
+
// V1 state shape: { args, uiState } — callers may pass either the full state
|
|
110
|
+
// or just the args object. Unwrap once for both V2 and V1 paths.
|
|
111
|
+
const value = data.args ?? data;
|
|
112
|
+
|
|
113
|
+
// Try V2 (BlockModelV3 storage facade) first, fall back to V1 (legacy setBlockArgs).
|
|
114
|
+
try {
|
|
115
|
+
await project.mutateBlockStorage(
|
|
116
|
+
blockId,
|
|
117
|
+
{ operation: "update-block-data", value },
|
|
118
|
+
ctx.getAuthorMarker(),
|
|
119
|
+
);
|
|
120
|
+
} catch (e: unknown) {
|
|
121
|
+
if (e instanceof ModelAPIVersionMismatchError) {
|
|
122
|
+
await project.setBlockArgs(blockId, value, ctx.getAuthorMarker());
|
|
123
|
+
} else {
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
112
127
|
return textResult({ ok: true });
|
|
113
128
|
},
|
|
114
129
|
);
|
|
@@ -137,19 +137,37 @@ export function registerUIInteractionTools(server: McpServer, ctx: ToolContext):
|
|
|
137
137
|
"execute_js",
|
|
138
138
|
{
|
|
139
139
|
description:
|
|
140
|
-
"Execute JavaScript in
|
|
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
|
-
|
|
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
|
);
|