@milaboratories/pl-mcp-server 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- /** Execute JavaScript in the renderer and return the result. */
15
- executeJavaScript?: (code: string) => Promise<unknown>;
14
+ /**
15
+ * Execute JavaScript in a renderer and return the result.
16
+ * With no `target`, runs in the topmost webContents (usually the main app).
17
+ * With `target`, runs in the specified block's webview (where `window.platforma` is
18
+ * exposed) — the block must already be loaded (e.g. via `select_block`).
19
+ */
20
+ executeJavaScript?: (code: string, target?: {
21
+ projectId: string;
22
+ blockId: string;
23
+ }) => Promise<unknown>;
16
24
  /** List available blocks from all configured registries. */
17
25
  listAvailableBlocks?: (query?: string) => Promise<unknown[]>;
18
26
  /** Navigate the desktop UI to show a specific block. */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;UAkBiB,oBAAA;EACf,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EACjD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EAHA;EAKjD,iBAAA,SAA0B,OAAA;EAHsB;EAKhD,cAAA,IAAkB,KAAA,cAAmB,OAAA;EAFX;EAI1B,iBAAA,IAAqB,IAAA,aAAiB,OAAA;EAAA;EAEtC,mBAAA,IAAuB,KAAA,cAAmB,OAAA;EAEY;EAAtD,WAAA,IAAe,SAAA,UAAmB,OAAA,aAAoB,OAAA;EAItB;EAFhC,UAAA,IAAc,KAAA,UAAe,MAAA,cAAoB,OAAA;EAQ5C;EANL,eAAA,SAAwB,OAAA,CAAQ,gBAAA;EAeb;EAbnB,eAAA,IACE,IAAA,UACA,KAAA,UACA,QAAA,cACG,OAAA;IAAU,MAAA;IAAgB,OAAA;EAAA;EAvBX;EAyBpB,mBAAA,SAA4B,OAAA;IAC1B,SAAA;IACA,IAAA;IACA,IAAA;IACA,KAAA;EAAA;EA3B8C;EA8BhD,UAAA,SAAmB,OAAA;EA7BC;EA+BpB,YAAA,IACE,WAAA,UACA,YAAA,UACA,IAAA,UACA,OAAA,aACG,OAAA;AAAA;AAAA,UAGU,gBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,aAAA;AAAA;;KAIU,SAAA,GAAY,OAAA;AAAA,UAEP,kBAAA;EAzC2B;EA2C1C,WAAA,GAAc,WAAA;EAzCC;EA2Cf,IAAA;EA3CsD;EA6CtD,MAAA,EAAQ,SAAA;EA3CM;EA6Cd,SAAA,GAAY,oBAAA;AAAA;AAAA,cAGD,WAAA;EAAA,QACH,EAAA;EAAA,QACA,IAAA;EAAA,iBACS,MAAA;EAAA,iBACA,SAAA;EAAA,QACT,UAAA;EAAA,iBACS,UAAA;cAEL,OAAA,EAAS,kBAAA;EAhDN;EAwDf,cAAA,CAAe,EAAA,EAAI,WAAA;EAAA,IAIf,GAAA,CAAA;EAIE,KAAA,CAAA,GAAS,OAAA;EAiGT,IAAA,CAAA,GAAQ,OAAA;EAAA,QAeN,eAAA;EAAA,QAOA,aAAA;EAjLN;EAAA,QAyMM,SAAA;EAtMW;EAAA,QA4ML,cAAA;EAzMZ;EAAA,QAmNY,gBAAA;AAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;UAkBiB,oBAAA;EACf,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EACjD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,eAAA,IAAmB,SAAA,oBAA6B,OAAA;EAChD,gBAAA,IAAoB,SAAA,oBAA6B,OAAA;EAHA;EAKjD,iBAAA,SAA0B,OAAA;EAHsB;EAKhD,cAAA,IAAkB,KAAA,cAAmB,OAAA;EAFX;;;;;;EAS1B,iBAAA,IACE,IAAA,UACA,MAAA;IAAW,SAAA;IAAmB,OAAA;EAAA,MAC3B,OAAA;EAuBc;EArBnB,mBAAA,IAAuB,KAAA,cAAmB,OAAA;EA4B9B;EA1BZ,WAAA,IAAe,SAAA,UAAmB,OAAA,aAAoB,OAAA;EArBtD;EAuBA,UAAA,IAAc,KAAA,UAAe,MAAA,cAAoB,OAAA;EAvBA;EAyBjD,eAAA,SAAwB,OAAA,CAAQ,gBAAA;EAxBb;EA0BnB,eAAA,IACE,IAAA,UACA,KAAA,UACA,QAAA,cACG,OAAA;IAAU,MAAA;IAAgB,OAAA;EAAA;EA5B/B;EA8BA,mBAAA,SAA4B,OAAA;IAC1B,SAAA;IACA,IAAA;IACA,IAAA;IACA,KAAA;EAAA;EA9BmC;EAiCrC,UAAA,SAAmB,OAAA;EAzBjB;EA2BF,YAAA,IACE,WAAA,UACA,YAAA,UACA,IAAA,UACA,OAAA,aACG,OAAA;AAAA;AAAA,UAGU,gBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,aAAA;AAAA;;KAIU,SAAA,GAAY,OAAA;AAAA,UAEP,kBAAA;EArCf;EAuCA,WAAA,GAAc,WAAA;EAvCe;EAyC7B,IAAA;EAvCA;EAyCA,MAAA,EAAQ,SAAA;EAzCwB;EA2ChC,SAAA,GAAY,oBAAA;AAAA;AAAA,cAGD,WAAA;EAAA,QACH,EAAA;EAAA,QACA,IAAA;EAAA,iBACS,MAAA;EAAA,iBACA,SAAA;EAAA,QACT,UAAA;EAAA,iBACS,UAAA;cAEL,OAAA,EAAS,kBAAA;EA5CnB;EAoDF,cAAA,CAAe,EAAA,EAAI,WAAA;EAAA,IAIf,GAAA,CAAA;EAIE,KAAA,CAAA,GAAS,OAAA;EAiGT,IAAA,CAAA,GAAQ,OAAA;EAAA,QAeN,eAAA;EAAA,QAOA,aAAA;EA1KN;EAAA,QAkMM,SAAA;EAhMN;EAAA,QAsMY,cAAA;EArMF;EAAA,QA+ME,gBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /** Execute JavaScript in the renderer and return the result. */\n executeJavaScript?: (code: string) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { type MiddleLayer, resourceIdToString } from \"@milaboratories/pl-middle-layer\";\nimport type { Branded } from \"@milaboratories/pl-model-common\";\nimport type { ToolContext } from \"./tools/types\";\nimport { registerPingTool } from \"./tools/ping\";\nimport { registerConnectionTools } from \"./tools/connection\";\nimport { registerProjectTools } from \"./tools/projects\";\nimport { registerBlockTools } from \"./tools/blocks\";\nimport { registerBlockStateTools } from \"./tools/block-state\";\nimport { registerAwaitTools } from \"./tools/await\";\nimport { registerLogTools } from \"./tools/logs\";\nimport { registerDataQueryTools } from \"./tools/data-query\";\nimport { registerScreenshotTool } from \"./tools/screenshot\";\nimport { registerUIInteractionTools } from \"./tools/ui-interaction\";\n\nexport interface PlMcpServerCallbacks {\n onProjectCreated?: (projectId: string) => void | Promise<void>;\n onProjectOpened?: (projectId: string) => void | Promise<void>;\n onProjectClosed?: (projectId: string) => void | Promise<void>;\n onProjectDeleted?: (projectId: string) => void | Promise<void>;\n /** Capture the current application window as a PNG screenshot. Returns base64-encoded PNG. */\n captureScreenshot?: () => Promise<string>;\n /** Send an input event to the application window. */\n sendInputEvent?: (event: unknown) => Promise<void>;\n /**\n * Execute JavaScript in a renderer and return the result.\n * With no `target`, runs in the topmost webContents (usually the main app).\n * With `target`, runs in the specified block's webview (where `window.platforma` is\n * exposed) — the block must already be loaded (e.g. via `select_block`).\n */\n executeJavaScript?: (\n code: string,\n target?: { projectId: string; blockId: string },\n ) => Promise<unknown>;\n /** List available blocks from all configured registries. */\n listAvailableBlocks?: (query?: string) => Promise<unknown[]>;\n /** Navigate the desktop UI to show a specific block. */\n selectBlock?: (projectId: string, blockId: string) => Promise<void>;\n /** Read recent lines from the application log. */\n readAppLog?: (lines: number, search?: string) => Promise<string>;\n /** List saved server connections. */\n listConnections?: () => Promise<ServerConnection[]>;\n /** Connect to a server. */\n connectToServer?: (\n addr: string,\n login: string,\n password?: string,\n ) => Promise<{ status: string; message: string }>;\n /** Get current connection status. */\n getConnectionStatus?: () => Promise<{\n connected: boolean;\n type?: string;\n addr?: string;\n login?: string;\n }>;\n /** Disconnect from current server. */\n disconnect?: () => Promise<void>;\n /** Get detailed info about a specific block package. */\n getBlockInfo?: (\n registryUrl: string,\n organization: string,\n name: string,\n version: string,\n ) => Promise<unknown>;\n}\n\nexport interface ServerConnection {\n addr: string;\n login: string;\n coreVersion?: string;\n lastConnected?: string;\n}\n\n/** Branded type for the MCP server URL secret path segment. */\nexport type McpSecret = Branded<string, \"McpSecret\">;\n\nexport interface PlMcpServerOptions {\n /** MiddleLayer instance providing access to projects, blocks, etc. Optional — server can start without it. */\n middleLayer?: MiddleLayer;\n /** Port to listen on. */\n port: number;\n /** Secret path segment for URL security. */\n secret: McpSecret;\n /** Optional callbacks for project lifecycle events (e.g. to sync UI state). */\n callbacks?: PlMcpServerCallbacks;\n}\n\nexport class PlMcpServer {\n private ml: MiddleLayer | null;\n private port: number;\n private readonly secret: McpSecret;\n private readonly callbacks: PlMcpServerCallbacks;\n private httpServer: Server | undefined;\n private readonly transports = new Map<string, StreamableHTTPServerTransport>();\n\n constructor(options: PlMcpServerOptions) {\n this.ml = options.middleLayer ?? null;\n this.port = options.port;\n this.secret = options.secret;\n this.callbacks = options.callbacks ?? {};\n }\n\n /** Set or update the MiddleLayer instance (e.g. after connecting to a server). */\n setMiddleLayer(ml: MiddleLayer | null | undefined) {\n this.ml = ml ?? null;\n }\n\n get url(): string {\n return `http://127.0.0.1:${this.port}/${this.secret}/mcp`;\n }\n\n async start(): Promise<void> {\n if (this.httpServer) {\n throw new Error(\"MCP server is already running\");\n }\n\n const expectedPath = `/${this.secret}/mcp`;\n\n this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n try {\n // Origin validation — only allow localhost\n if (req.headers.origin !== undefined) {\n try {\n const origin = new URL(req.headers.origin);\n if (origin.hostname !== \"localhost\" && origin.hostname !== \"127.0.0.1\") {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n } catch {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Forbidden\" }));\n return;\n }\n }\n\n // Secret path check\n if (req.url !== expectedPath) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n // Route to existing session transport\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (sessionId && this.transports.has(sessionId)) {\n await this.transports.get(sessionId)!.handleRequest(req, res);\n return;\n }\n\n // New session — create transport and connect MCP server\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) this.transports.delete(sid);\n };\n\n const server = this.createMcpServer();\n await server.connect(transport);\n await transport.handleRequest(req, res);\n\n const sid = transport.sessionId;\n if (sid) this.transports.set(sid, transport);\n } catch {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n }\n });\n\n const maxRetries = 10;\n const requestHandler = this.httpServer.listeners(\"request\")[0] as (\n req: IncomingMessage,\n res: ServerResponse,\n ) => void;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const server = this.httpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.listen(this.port, \"127.0.0.1\", () => resolve());\n server.once(\"error\", reject);\n });\n // Read back the actual port (important when port is 0)\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n return;\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n err.code === \"EADDRINUSE\" &&\n attempt < maxRetries - 1\n ) {\n server.removeAllListeners();\n this.httpServer = createServer(requestHandler);\n this.port++;\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const transport of this.transports.values()) {\n await transport.close();\n }\n this.transports.clear();\n\n const server = this.httpServer;\n if (server) {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n this.httpServer = undefined;\n }\n }\n\n private createMcpServer(): McpServer {\n const sessionId = randomUUID().slice(0, 8);\n const server = new McpServer({ name: \"pl\", version: \"0.1.0\" }, { capabilities: { tools: {} } });\n this.registerTools(server, sessionId);\n return server;\n }\n\n private registerTools(server: McpServer, sessionId: string): void {\n const authorId = `mcp-${sessionId}`;\n let localVersion = 0;\n const ctx: ToolContext = {\n getMl: () => this.ml,\n requireMl: () => this.requireMl(),\n resolveProject: (id) => this.resolveProject(id),\n getOpenedProject: (id) => this.getOpenedProject(id),\n callbacks: this.callbacks,\n getAuthorMarker: () => ({ authorId, localVersion: ++localVersion }),\n };\n registerPingTool(server, ctx);\n registerConnectionTools(server, ctx);\n registerProjectTools(server, ctx);\n registerBlockTools(server, ctx);\n registerBlockStateTools(server, ctx);\n registerAwaitTools(server, ctx);\n registerLogTools(server, ctx);\n registerDataQueryTools(server, ctx);\n registerScreenshotTool(server, ctx);\n registerUIInteractionTools(server, ctx);\n }\n\n /** Throws if MiddleLayer is not available (not connected to a server). */\n private requireMl(): MiddleLayer {\n if (!this.ml) throw new Error(\"Not connected to a server. Use connect_to_server first.\");\n return this.ml;\n }\n\n /** Resolves a project from the list by its projectId (resourceIdToString format). */\n private async resolveProject(projectId: string) {\n const ml = this.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n const entry = projects.find((p) => resourceIdToString(p.rid) === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. Resolves via project list → rid → getOpenedProject. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.rid);\n }\n}\n"],"mappings":"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"}
@@ -104,11 +104,20 @@ function registerUIInteractionTools(server, ctx) {
104
104
  return require_types.textResult({ ok: true });
105
105
  });
106
106
  server.registerTool("execute_js", {
107
- description: "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
108
- inputSchema: { code: zod.z.string().describe("JavaScript code to execute") }
109
- }, async ({ code }) => {
107
+ description: "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
108
+ inputSchema: {
109
+ code: zod.z.string().describe("JavaScript code to execute"),
110
+ projectId: zod.z.string().optional().describe("Target project ID. Must be paired with blockId."),
111
+ blockId: zod.z.string().optional().describe("Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).")
112
+ }
113
+ }, async ({ code, projectId, blockId }) => {
110
114
  if (!ctx.callbacks.executeJavaScript) return require_types.errorResult("JS execution is not available.", "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log");
111
- return require_types.textResult(await ctx.callbacks.executeJavaScript(code));
115
+ if (projectId === void 0 !== (blockId === void 0)) return require_types.errorResult("projectId and blockId must be provided together.", "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.");
116
+ const target = projectId !== void 0 && blockId !== void 0 ? {
117
+ projectId,
118
+ blockId
119
+ } : void 0;
120
+ return require_types.textResult(await ctx.callbacks.executeJavaScript(code, target));
112
121
  });
113
122
  }
114
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ui-interaction.cjs","names":["z","errorResult","textResult"],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n },\n },\n async ({ code }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const result = await ctx.callbacks.executeJavaScript(code);\n return textResult(result);\n },\n );\n}\n"],"mappings":";;;AAKA,SAAgB,2BAA2B,QAAmB,KAAwB;AACpF,QAAO,aACL,SACA;EACE,aACE;EACF,aAAa;GACX,GAAGA,IAAAA,EAAE,QAAQ,CAAC,SAAS,eAAe;GACtC,GAAGA,IAAAA,EAAE,QAAQ,CAAC,SAAS,eAAe;GACtC,aAAaA,IAAAA,EAAE,SAAS,CAAC,UAAU,CAAC,SAAS,eAAe;GAC7D;EACF,EACD,OAAO,EAAE,GAAG,GAAG,kBAAkB;AAC/B,MAAI,CAAC,IAAI,UAAU,eACjB,QAAOC,cAAAA,YACL,oCACA,sJACD;EAEH,MAAM,aAAa,cAAc,IAAI;AACrC,QAAM,IAAI,UAAU,eAAe;GACjC,MAAM;GACN;GACA;GACA,QAAQ;GACR;GACD,CAAC;AACF,QAAM,IAAI,UAAU,eAAe;GAAE,MAAM;GAAW;GAAG;GAAG,QAAQ;GAAQ;GAAY,CAAC;AACzF,SAAOC,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,aACA;EACE,aAAa;EACb,aAAa,EACX,MAAMF,IAAAA,EAAE,QAAQ,CAAC,SAAS,eAAe,EAC1C;EACF,EACD,OAAO,EAAE,WAAW;AAClB,MAAI,CAAC,IAAI,UAAU,eACjB,QAAOC,cAAAA,YACL,oCACA,sJACD;AAEH,OAAK,MAAM,QAAQ,MAAM;AACvB,SAAM,IAAI,UAAU,eAAe;IAAE,MAAM;IAAW,SAAS;IAAM,CAAC;AACtE,SAAM,IAAI,UAAU,eAAe;IAAE,MAAM;IAAQ,SAAS;IAAM,CAAC;AACnE,SAAM,IAAI,UAAU,eAAe;IAAE,MAAM;IAAS,SAAS;IAAM,CAAC;;AAEtE,SAAOC,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,aACA;EACE,aAAa;EACb,aAAa;GACX,KAAKF,IAAAA,EACF,QAAQ,CACR,SAAS,qEAAqE;GACjF,WAAWA,IAAAA,EACR,MAAMA,IAAAA,EAAE,KAAK;IAAC;IAAS;IAAW;IAAO;IAAO,CAAC,CAAC,CAClD,UAAU,CACV,SAAS,wBAAwB;GACrC;EACF,EACD,OAAO,EAAE,KAAK,gBAAgB;AAC5B,MAAI,CAAC,IAAI,UAAU,eACjB,QAAOC,cAAAA,YACL,oCACA,sJACD;AAEH,QAAM,IAAI,UAAU,eAAe;GACjC,MAAM;GACN,SAAS;GACT,GAAI,aAAa;IACf,OAAO,UAAU,SAAS,QAAQ;IAClC,SAAS,UAAU,SAAS,UAAU;IACtC,KAAK,UAAU,SAAS,MAAM;IAC9B,MAAM,UAAU,SAAS,OAAO;IACjC;GACF,CAAC;AACF,QAAM,IAAI,UAAU,eAAe;GACjC,MAAM;GACN,SAAS;GACT,GAAI,aAAa;IACf,OAAO,UAAU,SAAS,QAAQ;IAClC,SAAS,UAAU,SAAS,UAAU;IACtC,KAAK,UAAU,SAAS,MAAM;IAC9B,MAAM,UAAU,SAAS,OAAO;IACjC;GACF,CAAC;AACF,SAAOC,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,UACA;EACE,aAAa;EACb,aAAa;GACX,GAAGF,IAAAA,EAAE,QAAQ,CAAC,SAAS,4BAA4B;GACnD,GAAGA,IAAAA,EAAE,QAAQ,CAAC,SAAS,4BAA4B;GACnD,QAAQA,IAAAA,EAAE,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,SAAS,2BAA2B;GAC7E,QAAQA,IAAAA,EAAE,QAAQ,CAAC,SAAS,0DAA0D;GACvF;EACF,EACD,OAAO,EAAE,GAAG,GAAG,QAAQ,aAAa;AAClC,MAAI,CAAC,IAAI,UAAU,eACjB,QAAOC,cAAAA,YACL,oCACA,sJACD;AAEH,QAAM,IAAI,UAAU,eAAe;GACjC,MAAM;GACN;GACA;GACA,QAAQ,UAAU;GAClB;GACD,CAAC;AACF,SAAOC,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,cACA;EACE,aACE;EACF,aAAa,EACX,MAAMF,IAAAA,EAAE,QAAQ,CAAC,SAAS,6BAA6B,EACxD;EACF,EACD,OAAO,EAAE,WAAW;AAClB,MAAI,CAAC,IAAI,UAAU,kBACjB,QAAOC,cAAAA,YACL,kCACA,sJACD;AAGH,SAAOC,cAAAA,WADQ,MAAM,IAAI,UAAU,kBAAkB,KAAK,CACjC;GAE5B"}
1
+ {"version":3,"file":"ui-interaction.cjs","names":["z","errorResult","textResult"],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n projectId: z\n .string()\n .optional()\n .describe(\"Target project ID. Must be paired with blockId.\"),\n blockId: z\n .string()\n .optional()\n .describe(\n \"Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).\",\n ),\n },\n },\n async ({ code, projectId, blockId }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n if ((projectId === undefined) !== (blockId === undefined)) {\n return errorResult(\n \"projectId and blockId must be provided together.\",\n \"Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.\",\n );\n }\n const target =\n projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;\n const result = await ctx.callbacks.executeJavaScript(code, target);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
@@ -104,11 +104,20 @@ function registerUIInteractionTools(server, ctx) {
104
104
  return textResult({ ok: true });
105
105
  });
106
106
  server.registerTool("execute_js", {
107
- description: "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
108
- inputSchema: { code: z.string().describe("JavaScript code to execute") }
109
- }, async ({ code }) => {
107
+ description: "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
108
+ inputSchema: {
109
+ code: z.string().describe("JavaScript code to execute"),
110
+ projectId: z.string().optional().describe("Target project ID. Must be paired with blockId."),
111
+ blockId: z.string().optional().describe("Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).")
112
+ }
113
+ }, async ({ code, projectId, blockId }) => {
110
114
  if (!ctx.callbacks.executeJavaScript) return errorResult("JS execution is not available.", "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log");
111
- return textResult(await ctx.callbacks.executeJavaScript(code));
115
+ if (projectId === void 0 !== (blockId === void 0)) return errorResult("projectId and blockId must be provided together.", "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.");
116
+ const target = projectId !== void 0 && blockId !== void 0 ? {
117
+ projectId,
118
+ blockId
119
+ } : void 0;
120
+ return textResult(await ctx.callbacks.executeJavaScript(code, target));
112
121
  });
113
122
  }
114
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ui-interaction.js","names":[],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n },\n },\n async ({ code }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const result = await ctx.callbacks.executeJavaScript(code);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ui-interaction.js","names":[],"sources":["../../src/tools/ui-interaction.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"click\",\n {\n description:\n \"Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.\",\n inputSchema: {\n x: z.number().describe(\"X coordinate\"),\n y: z.number().describe(\"Y coordinate\"),\n doubleClick: z.boolean().optional().describe(\"Double click\"),\n },\n },\n async ({ x, y, doubleClick }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n const clickCount = doubleClick ? 2 : 1;\n await ctx.callbacks.sendInputEvent({\n type: \"mouseDown\",\n x,\n y,\n button: \"left\",\n clickCount,\n });\n await ctx.callbacks.sendInputEvent({ type: \"mouseUp\", x, y, button: \"left\", clickCount });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"type_text\",\n {\n description: \"Type text into the currently focused element\",\n inputSchema: {\n text: z.string().describe(\"Text to type\"),\n },\n },\n async ({ text }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n for (const char of text) {\n await ctx.callbacks.sendInputEvent({ type: \"keyDown\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"char\", keyCode: char });\n await ctx.callbacks.sendInputEvent({ type: \"keyUp\", keyCode: char });\n }\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"press_key\",\n {\n description: \"Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)\",\n inputSchema: {\n key: z\n .string()\n .describe(\"Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')\"),\n modifiers: z\n .array(z.enum([\"shift\", \"control\", \"alt\", \"meta\"]))\n .optional()\n .describe(\"Modifier keys to hold\"),\n },\n },\n async ({ key, modifiers }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"keyDown\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n await ctx.callbacks.sendInputEvent({\n type: \"keyUp\",\n keyCode: key,\n ...(modifiers && {\n shift: modifiers.includes(\"shift\"),\n control: modifiers.includes(\"control\"),\n alt: modifiers.includes(\"alt\"),\n meta: modifiers.includes(\"meta\"),\n }),\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"scroll\",\n {\n description: \"Scroll the page at a given position\",\n inputSchema: {\n x: z.number().describe(\"X coordinate to scroll at\"),\n y: z.number().describe(\"Y coordinate to scroll at\"),\n deltaX: z.number().optional().default(0).describe(\"Horizontal scroll amount\"),\n deltaY: z.number().describe(\"Vertical scroll amount (negative = up, positive = down)\"),\n },\n },\n async ({ x, y, deltaX, deltaY }) => {\n if (!ctx.callbacks.sendInputEvent) {\n return errorResult(\n \"UI interaction is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n await ctx.callbacks.sendInputEvent({\n type: \"mouseWheel\",\n x,\n y,\n deltaX: deltaX ?? 0,\n deltaY,\n });\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"execute_js\",\n {\n description:\n \"Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.\",\n inputSchema: {\n code: z.string().describe(\"JavaScript code to execute\"),\n projectId: z\n .string()\n .optional()\n .describe(\"Target project ID. Must be paired with blockId.\"),\n blockId: z\n .string()\n .optional()\n .describe(\n \"Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).\",\n ),\n },\n },\n async ({ code, projectId, blockId }) => {\n if (!ctx.callbacks.executeJavaScript) {\n return errorResult(\n \"JS execution is not available.\",\n \"Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log\",\n );\n }\n if ((projectId === undefined) !== (blockId === undefined)) {\n return errorResult(\n \"projectId and blockId must be provided together.\",\n \"Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.\",\n );\n }\n const target =\n projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;\n const result = await ctx.callbacks.executeJavaScript(code, target);\n return textResult(result);\n },\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-mcp-server",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for Platforma Desktop",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -28,17 +28,17 @@
28
28
  "@vitest/coverage-istanbul": "^4.1.3",
29
29
  "typescript": "~5.9.3",
30
30
  "vitest": "^4.1.3",
31
- "@milaboratories/pl-model-common": "1.31.2",
31
+ "@milaboratories/pl-errors": "1.3.0",
32
32
  "@milaboratories/build-configs": "2.0.0",
33
33
  "@milaboratories/ts-configs": "1.2.3",
34
- "@milaboratories/pl-middle-layer": "1.55.11",
35
- "@milaboratories/ts-builder": "1.3.2",
36
- "@milaboratories/pl-errors": "1.2.8",
37
- "@platforma-sdk/model": "1.65.0"
34
+ "@milaboratories/pl-middle-layer": "1.55.14",
35
+ "@milaboratories/pl-model-common": "1.31.2",
36
+ "@platforma-sdk/model": "1.65.4",
37
+ "@milaboratories/ts-builder": "1.3.2"
38
38
  },
39
39
  "peerDependencies": {
40
- "@milaboratories/pl-middle-layer": ">=1.55.11",
41
- "@platforma-sdk/model": ">=1.65.0"
40
+ "@milaboratories/pl-middle-layer": ">=1.55.14",
41
+ "@platforma-sdk/model": ">=1.65.4"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=22.0.0"
package/src/server.ts CHANGED
@@ -25,8 +25,16 @@ export interface PlMcpServerCallbacks {
25
25
  captureScreenshot?: () => Promise<string>;
26
26
  /** Send an input event to the application window. */
27
27
  sendInputEvent?: (event: unknown) => Promise<void>;
28
- /** Execute JavaScript in the renderer and return the result. */
29
- executeJavaScript?: (code: string) => Promise<unknown>;
28
+ /**
29
+ * Execute JavaScript in a renderer and return the result.
30
+ * With no `target`, runs in the topmost webContents (usually the main app).
31
+ * With `target`, runs in the specified block's webview (where `window.platforma` is
32
+ * exposed) — the block must already be loaded (e.g. via `select_block`).
33
+ */
34
+ executeJavaScript?: (
35
+ code: string,
36
+ target?: { projectId: string; blockId: string },
37
+ ) => Promise<unknown>;
30
38
  /** List available blocks from all configured registries. */
31
39
  listAvailableBlocks?: (query?: string) => Promise<unknown[]>;
32
40
  /** Navigate the desktop UI to show a specific block. */
@@ -137,19 +137,37 @@ export function registerUIInteractionTools(server: McpServer, ctx: ToolContext):
137
137
  "execute_js",
138
138
  {
139
139
  description:
140
- "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
140
+ "Execute JavaScript in a renderer and return the result. By default runs in the topmost webContents (main app / topmost modal). Pass projectId + blockId to run inside that block's webview, where `window.platforma` is exposed and the driverKit (e.g. `window.platforma.lsDriver.getLocalFileHandle`) is callable. The block must already be open — call `select_block` first if needed.",
141
141
  inputSchema: {
142
142
  code: z.string().describe("JavaScript code to execute"),
143
+ projectId: z
144
+ .string()
145
+ .optional()
146
+ .describe("Target project ID. Must be paired with blockId."),
147
+ blockId: z
148
+ .string()
149
+ .optional()
150
+ .describe(
151
+ "Target block ID. When provided with projectId, JS runs in that block's webview (where `window.platforma` is available).",
152
+ ),
143
153
  },
144
154
  },
145
- async ({ code }) => {
155
+ async ({ code, projectId, blockId }) => {
146
156
  if (!ctx.callbacks.executeJavaScript) {
147
157
  return errorResult(
148
158
  "JS execution is not available.",
149
159
  "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
150
160
  );
151
161
  }
152
- const result = await ctx.callbacks.executeJavaScript(code);
162
+ if ((projectId === undefined) !== (blockId === undefined)) {
163
+ return errorResult(
164
+ "projectId and blockId must be provided together.",
165
+ "Either pass both to target a specific block's webview, or pass neither to run in the topmost webContents.",
166
+ );
167
+ }
168
+ const target =
169
+ projectId !== undefined && blockId !== undefined ? { projectId, blockId } : undefined;
170
+ const result = await ctx.callbacks.executeJavaScript(code, target);
153
171
  return textResult(result);
154
172
  },
155
173
  );