@milaboratories/pl-mcp-server 9.0.0 → 11.0.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/dist/server.cjs +4 -5
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -5
- package/dist/server.js.map +1 -1
- package/dist/tools/await.cjs +1 -1
- package/dist/tools/await.js +1 -1
- package/dist/tools/projects.cjs +4 -5
- package/dist/tools/projects.cjs.map +1 -1
- package/dist/tools/projects.js +4 -5
- package/dist/tools/projects.js.map +1 -1
- package/package.json +8 -8
- package/src/server.ts +5 -5
- package/src/tools/projects.ts +4 -6
package/dist/server.cjs
CHANGED
|
@@ -12,7 +12,6 @@ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk
|
|
|
12
12
|
let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
13
13
|
let node_http = require("node:http");
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
15
|
-
let _milaboratories_pl_middle_layer = require("@milaboratories/pl-middle-layer");
|
|
16
15
|
//#region src/server.ts
|
|
17
16
|
var PlMcpServer = class {
|
|
18
17
|
ml;
|
|
@@ -150,19 +149,19 @@ var PlMcpServer = class {
|
|
|
150
149
|
if (!this.ml) throw new Error("Not connected to a server. Use connect_to_server first.");
|
|
151
150
|
return this.ml;
|
|
152
151
|
}
|
|
153
|
-
/** Resolves a project from the list by its projectId
|
|
152
|
+
/** Resolves a project from the list by its projectId. */
|
|
154
153
|
async resolveProject(projectId) {
|
|
155
154
|
const ml = this.requireMl();
|
|
156
155
|
await ml.projectList.refreshState();
|
|
157
|
-
const entry = (await ml.projectList.awaitStableValue()).find((p) =>
|
|
156
|
+
const entry = (await ml.projectList.awaitStableValue()).find((p) => p.id === projectId);
|
|
158
157
|
if (!entry) throw new Error(`Project ${projectId} not found`);
|
|
159
158
|
return entry;
|
|
160
159
|
}
|
|
161
|
-
/** Gets an opened project by projectId.
|
|
160
|
+
/** Gets an opened project by projectId. */
|
|
162
161
|
async getOpenedProject(projectId) {
|
|
163
162
|
const ml = this.requireMl();
|
|
164
163
|
const entry = await this.resolveProject(projectId);
|
|
165
|
-
return ml.getOpenedProject(entry.
|
|
164
|
+
return ml.getOpenedProject(entry.id);
|
|
166
165
|
}
|
|
167
166
|
};
|
|
168
167
|
//#endregion
|
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 /**\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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"}
|
|
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 } 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. */\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) => p.id === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.id);\n }\n}\n"],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -81,9 +81,9 @@ declare class PlMcpServer {
|
|
|
81
81
|
private registerTools;
|
|
82
82
|
/** Throws if MiddleLayer is not available (not connected to a server). */
|
|
83
83
|
private requireMl;
|
|
84
|
-
/** Resolves a project from the list by its projectId
|
|
84
|
+
/** Resolves a project from the list by its projectId. */
|
|
85
85
|
private resolveProject;
|
|
86
|
-
/** Gets an opened project by projectId.
|
|
86
|
+
/** Gets an opened project by projectId. */
|
|
87
87
|
private getOpenedProject;
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
package/dist/server.js
CHANGED
|
@@ -12,7 +12,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
13
|
import { createServer } from "node:http";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
|
-
import { resourceIdToString } from "@milaboratories/pl-middle-layer";
|
|
16
15
|
//#region src/server.ts
|
|
17
16
|
var PlMcpServer = class {
|
|
18
17
|
ml;
|
|
@@ -150,19 +149,19 @@ var PlMcpServer = class {
|
|
|
150
149
|
if (!this.ml) throw new Error("Not connected to a server. Use connect_to_server first.");
|
|
151
150
|
return this.ml;
|
|
152
151
|
}
|
|
153
|
-
/** Resolves a project from the list by its projectId
|
|
152
|
+
/** Resolves a project from the list by its projectId. */
|
|
154
153
|
async resolveProject(projectId) {
|
|
155
154
|
const ml = this.requireMl();
|
|
156
155
|
await ml.projectList.refreshState();
|
|
157
|
-
const entry = (await ml.projectList.awaitStableValue()).find((p) =>
|
|
156
|
+
const entry = (await ml.projectList.awaitStableValue()).find((p) => p.id === projectId);
|
|
158
157
|
if (!entry) throw new Error(`Project ${projectId} not found`);
|
|
159
158
|
return entry;
|
|
160
159
|
}
|
|
161
|
-
/** Gets an opened project by projectId.
|
|
160
|
+
/** Gets an opened project by projectId. */
|
|
162
161
|
async getOpenedProject(projectId) {
|
|
163
162
|
const ml = this.requireMl();
|
|
164
163
|
const entry = await this.resolveProject(projectId);
|
|
165
|
-
return ml.getOpenedProject(entry.
|
|
164
|
+
return ml.getOpenedProject(entry.id);
|
|
166
165
|
}
|
|
167
166
|
};
|
|
168
167
|
//#endregion
|
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 /**\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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"}
|
|
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 } 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. */\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) => p.id === projectId);\n if (!entry) throw new Error(`Project ${projectId} not found`);\n return entry;\n }\n\n /** Gets an opened project by projectId. */\n private async getOpenedProject(projectId: string) {\n const ml = this.requireMl();\n const entry = await this.resolveProject(projectId);\n return ml.getOpenedProject(entry.id);\n }\n}\n"],"mappings":"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"}
|
package/dist/tools/await.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const require_types = require("./types.cjs");
|
|
2
2
|
const require_tokens = require("./tokens.cjs");
|
|
3
3
|
const require_sandbox = require("./sandbox.cjs");
|
|
4
|
-
let _milaboratories_pl_middle_layer = require("@milaboratories/pl-middle-layer");
|
|
5
4
|
let zod = require("zod");
|
|
6
5
|
let _platforma_sdk_model = require("@platforma-sdk/model");
|
|
6
|
+
let _milaboratories_pl_middle_layer = require("@milaboratories/pl-middle-layer");
|
|
7
7
|
//#region src/tools/await.ts
|
|
8
8
|
function registerAwaitTools(server, ctx) {
|
|
9
9
|
server.registerTool("await_block_done", {
|
package/dist/tools/await.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { errorResult, textResult } from "./types.js";
|
|
2
2
|
import { summarizeOutputs } from "./tokens.js";
|
|
3
3
|
import { safeEval } from "./sandbox.js";
|
|
4
|
-
import { isTimeoutError } from "@milaboratories/pl-middle-layer";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
import { deriveDataFromStorage } from "@platforma-sdk/model";
|
|
6
|
+
import { isTimeoutError } from "@milaboratories/pl-middle-layer";
|
|
7
7
|
//#region src/tools/await.ts
|
|
8
8
|
function registerAwaitTools(server, ctx) {
|
|
9
9
|
server.registerTool("await_block_done", {
|
package/dist/tools/projects.cjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const require_types = require("./types.cjs");
|
|
2
|
-
let _milaboratories_pl_middle_layer = require("@milaboratories/pl-middle-layer");
|
|
3
2
|
let zod = require("zod");
|
|
4
3
|
//#region src/tools/projects.ts
|
|
5
4
|
function registerProjectTools(server, ctx) {
|
|
@@ -7,7 +6,7 @@ function registerProjectTools(server, ctx) {
|
|
|
7
6
|
const ml = ctx.requireMl();
|
|
8
7
|
await ml.projectList.refreshState();
|
|
9
8
|
return require_types.textResult((await ml.projectList.awaitStableValue()).map((p) => ({
|
|
10
|
-
projectId:
|
|
9
|
+
projectId: p.id,
|
|
11
10
|
label: p.meta.label,
|
|
12
11
|
opened: p.opened,
|
|
13
12
|
created: p.created.toISOString(),
|
|
@@ -18,7 +17,7 @@ function registerProjectTools(server, ctx) {
|
|
|
18
17
|
description: "Create a new project",
|
|
19
18
|
inputSchema: { label: zod.z.string().describe("Project name") }
|
|
20
19
|
}, async ({ label }) => {
|
|
21
|
-
const projectId =
|
|
20
|
+
const projectId = await ctx.requireMl().createProject({ label });
|
|
22
21
|
await ctx.callbacks.onProjectCreated?.(projectId);
|
|
23
22
|
return require_types.textResult({ projectId });
|
|
24
23
|
});
|
|
@@ -27,7 +26,7 @@ function registerProjectTools(server, ctx) {
|
|
|
27
26
|
inputSchema: { projectId: zod.z.string().describe("Project ID from list_projects or create_project") }
|
|
28
27
|
}, async ({ projectId }) => {
|
|
29
28
|
const entry = await ctx.resolveProject(projectId);
|
|
30
|
-
await ctx.requireMl().openProject(entry.
|
|
29
|
+
await ctx.requireMl().openProject(entry.id);
|
|
31
30
|
await ctx.callbacks.onProjectOpened?.(projectId);
|
|
32
31
|
return require_types.textResult({ ok: true });
|
|
33
32
|
});
|
|
@@ -36,7 +35,7 @@ function registerProjectTools(server, ctx) {
|
|
|
36
35
|
inputSchema: { projectId: zod.z.string().describe("Project ID") }
|
|
37
36
|
}, async ({ projectId }) => {
|
|
38
37
|
const entry = await ctx.resolveProject(projectId);
|
|
39
|
-
await ctx.requireMl().closeProject(entry.
|
|
38
|
+
await ctx.requireMl().closeProject(entry.id);
|
|
40
39
|
await ctx.callbacks.onProjectClosed?.(projectId);
|
|
41
40
|
return require_types.textResult({ ok: true });
|
|
42
41
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.cjs","names":["textResult","z"],"sources":["../../src/tools/projects.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"projects.cjs","names":["textResult","z"],"sources":["../../src/tools/projects.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { textResult } from \"./types\";\n\nexport function registerProjectTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"list_projects\",\n { description: \"List all projects with their IDs, labels, and status\" },\n async () => {\n const ml = ctx.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n return textResult(\n projects.map((p) => ({\n projectId: p.id,\n label: p.meta.label,\n opened: p.opened,\n created: p.created.toISOString(),\n lastModified: p.lastModified.toISOString(),\n })),\n );\n },\n );\n\n server.registerTool(\n \"create_project\",\n {\n description: \"Create a new project\",\n inputSchema: { label: z.string().describe(\"Project name\") },\n },\n async ({ label }) => {\n const projectId = await ctx.requireMl().createProject({ label });\n await ctx.callbacks.onProjectCreated?.(projectId);\n return textResult({ projectId });\n },\n );\n\n server.registerTool(\n \"open_project\",\n {\n description: \"Open a project for editing. Required before working with blocks.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID from list_projects or create_project\"),\n },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().openProject(entry.id);\n await ctx.callbacks.onProjectOpened?.(projectId);\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"close_project\",\n {\n description: \"Close an opened project, releasing its resources\",\n inputSchema: { projectId: z.string().describe(\"Project ID\") },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().closeProject(entry.id);\n await ctx.callbacks.onProjectClosed?.(projectId);\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"delete_project\",\n {\n description: \"Delete a project permanently. The project must be closed first.\",\n inputSchema: { projectId: z.string().describe(\"Project ID\") },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().deleteProject(entry.id);\n await ctx.callbacks.onProjectDeleted?.(projectId);\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;AAKA,SAAgB,qBAAqB,QAAmB,KAAwB;AAC9E,QAAO,aACL,iBACA,EAAE,aAAa,wDAAwD,EACvE,YAAY;EACV,MAAM,KAAK,IAAI,WAAW;AAC1B,QAAM,GAAG,YAAY,cAAc;AAEnC,SAAOA,cAAAA,YADU,MAAM,GAAG,YAAY,kBAAkB,EAE7C,KAAK,OAAO;GACnB,WAAW,EAAE;GACb,OAAO,EAAE,KAAK;GACd,QAAQ,EAAE;GACV,SAAS,EAAE,QAAQ,aAAa;GAChC,cAAc,EAAE,aAAa,aAAa;GAC3C,EAAE,CACJ;GAEJ;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAOC,IAAAA,EAAE,QAAQ,CAAC,SAAS,eAAe,EAAE;EAC5D,EACD,OAAO,EAAE,YAAY;EACnB,MAAM,YAAY,MAAM,IAAI,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC;AAChE,QAAM,IAAI,UAAU,mBAAmB,UAAU;AACjD,SAAOD,cAAAA,WAAW,EAAE,WAAW,CAAC;GAEnC;AAED,QAAO,aACL,gBACA;EACE,aAAa;EACb,aAAa,EACX,WAAWC,IAAAA,EAAE,QAAQ,CAAC,SAAS,kDAAkD,EAClF;EACF,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,YAAY,MAAM,GAAG;AAC3C,QAAM,IAAI,UAAU,kBAAkB,UAAU;AAChD,SAAOD,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,iBACA;EACE,aAAa;EACb,aAAa,EAAE,WAAWC,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa,EAAE;EAC9D,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,aAAa,MAAM,GAAG;AAC5C,QAAM,IAAI,UAAU,kBAAkB,UAAU;AAChD,SAAOD,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,WAAWC,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa,EAAE;EAC9D,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,cAAc,MAAM,GAAG;AAC7C,QAAM,IAAI,UAAU,mBAAmB,UAAU;AACjD,SAAOD,cAAAA,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
|
package/dist/tools/projects.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { textResult } from "./types.js";
|
|
2
|
-
import { resourceIdToString } from "@milaboratories/pl-middle-layer";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
//#region src/tools/projects.ts
|
|
5
4
|
function registerProjectTools(server, ctx) {
|
|
@@ -7,7 +6,7 @@ function registerProjectTools(server, ctx) {
|
|
|
7
6
|
const ml = ctx.requireMl();
|
|
8
7
|
await ml.projectList.refreshState();
|
|
9
8
|
return textResult((await ml.projectList.awaitStableValue()).map((p) => ({
|
|
10
|
-
projectId:
|
|
9
|
+
projectId: p.id,
|
|
11
10
|
label: p.meta.label,
|
|
12
11
|
opened: p.opened,
|
|
13
12
|
created: p.created.toISOString(),
|
|
@@ -18,7 +17,7 @@ function registerProjectTools(server, ctx) {
|
|
|
18
17
|
description: "Create a new project",
|
|
19
18
|
inputSchema: { label: z.string().describe("Project name") }
|
|
20
19
|
}, async ({ label }) => {
|
|
21
|
-
const projectId =
|
|
20
|
+
const projectId = await ctx.requireMl().createProject({ label });
|
|
22
21
|
await ctx.callbacks.onProjectCreated?.(projectId);
|
|
23
22
|
return textResult({ projectId });
|
|
24
23
|
});
|
|
@@ -27,7 +26,7 @@ function registerProjectTools(server, ctx) {
|
|
|
27
26
|
inputSchema: { projectId: z.string().describe("Project ID from list_projects or create_project") }
|
|
28
27
|
}, async ({ projectId }) => {
|
|
29
28
|
const entry = await ctx.resolveProject(projectId);
|
|
30
|
-
await ctx.requireMl().openProject(entry.
|
|
29
|
+
await ctx.requireMl().openProject(entry.id);
|
|
31
30
|
await ctx.callbacks.onProjectOpened?.(projectId);
|
|
32
31
|
return textResult({ ok: true });
|
|
33
32
|
});
|
|
@@ -36,7 +35,7 @@ function registerProjectTools(server, ctx) {
|
|
|
36
35
|
inputSchema: { projectId: z.string().describe("Project ID") }
|
|
37
36
|
}, async ({ projectId }) => {
|
|
38
37
|
const entry = await ctx.resolveProject(projectId);
|
|
39
|
-
await ctx.requireMl().closeProject(entry.
|
|
38
|
+
await ctx.requireMl().closeProject(entry.id);
|
|
40
39
|
await ctx.callbacks.onProjectClosed?.(projectId);
|
|
41
40
|
return textResult({ ok: true });
|
|
42
41
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.js","names":[],"sources":["../../src/tools/projects.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"projects.js","names":[],"sources":["../../src/tools/projects.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { textResult } from \"./types\";\n\nexport function registerProjectTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"list_projects\",\n { description: \"List all projects with their IDs, labels, and status\" },\n async () => {\n const ml = ctx.requireMl();\n await ml.projectList.refreshState();\n const projects = await ml.projectList.awaitStableValue();\n return textResult(\n projects.map((p) => ({\n projectId: p.id,\n label: p.meta.label,\n opened: p.opened,\n created: p.created.toISOString(),\n lastModified: p.lastModified.toISOString(),\n })),\n );\n },\n );\n\n server.registerTool(\n \"create_project\",\n {\n description: \"Create a new project\",\n inputSchema: { label: z.string().describe(\"Project name\") },\n },\n async ({ label }) => {\n const projectId = await ctx.requireMl().createProject({ label });\n await ctx.callbacks.onProjectCreated?.(projectId);\n return textResult({ projectId });\n },\n );\n\n server.registerTool(\n \"open_project\",\n {\n description: \"Open a project for editing. Required before working with blocks.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID from list_projects or create_project\"),\n },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().openProject(entry.id);\n await ctx.callbacks.onProjectOpened?.(projectId);\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"close_project\",\n {\n description: \"Close an opened project, releasing its resources\",\n inputSchema: { projectId: z.string().describe(\"Project ID\") },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().closeProject(entry.id);\n await ctx.callbacks.onProjectClosed?.(projectId);\n return textResult({ ok: true });\n },\n );\n\n server.registerTool(\n \"delete_project\",\n {\n description: \"Delete a project permanently. The project must be closed first.\",\n inputSchema: { projectId: z.string().describe(\"Project ID\") },\n },\n async ({ projectId }) => {\n const entry = await ctx.resolveProject(projectId);\n await ctx.requireMl().deleteProject(entry.id);\n await ctx.callbacks.onProjectDeleted?.(projectId);\n return textResult({ ok: true });\n },\n );\n}\n"],"mappings":";;;AAKA,SAAgB,qBAAqB,QAAmB,KAAwB;AAC9E,QAAO,aACL,iBACA,EAAE,aAAa,wDAAwD,EACvE,YAAY;EACV,MAAM,KAAK,IAAI,WAAW;AAC1B,QAAM,GAAG,YAAY,cAAc;AAEnC,SAAO,YADU,MAAM,GAAG,YAAY,kBAAkB,EAE7C,KAAK,OAAO;GACnB,WAAW,EAAE;GACb,OAAO,EAAE,KAAK;GACd,QAAQ,EAAE;GACV,SAAS,EAAE,QAAQ,aAAa;GAChC,cAAc,EAAE,aAAa,aAAa;GAC3C,EAAE,CACJ;GAEJ;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,eAAe,EAAE;EAC5D,EACD,OAAO,EAAE,YAAY;EACnB,MAAM,YAAY,MAAM,IAAI,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC;AAChE,QAAM,IAAI,UAAU,mBAAmB,UAAU;AACjD,SAAO,WAAW,EAAE,WAAW,CAAC;GAEnC;AAED,QAAO,aACL,gBACA;EACE,aAAa;EACb,aAAa,EACX,WAAW,EAAE,QAAQ,CAAC,SAAS,kDAAkD,EAClF;EACF,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,YAAY,MAAM,GAAG;AAC3C,QAAM,IAAI,UAAU,kBAAkB,UAAU;AAChD,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,iBACA;EACE,aAAa;EACb,aAAa,EAAE,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa,EAAE;EAC9D,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,aAAa,MAAM,GAAG;AAC5C,QAAM,IAAI,UAAU,kBAAkB,UAAU;AAChD,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC;AAED,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa,EAAE;EAC9D,EACD,OAAO,EAAE,gBAAgB;EACvB,MAAM,QAAQ,MAAM,IAAI,eAAe,UAAU;AACjD,QAAM,IAAI,WAAW,CAAC,cAAc,MAAM,GAAG;AAC7C,QAAM,IAAI,UAAU,mBAAmB,UAAU;AACjD,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAElC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "MCP server for Platforma Desktop",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -29,16 +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.
|
|
32
|
+
"@milaboratories/pl-errors": "1.4.1",
|
|
33
|
+
"@milaboratories/pl-middle-layer": "1.59.1",
|
|
34
|
+
"@milaboratories/pl-model-common": "1.41.0",
|
|
35
|
+
"@milaboratories/ts-builder": "1.4.0",
|
|
33
36
|
"@milaboratories/ts-configs": "1.2.3",
|
|
34
|
-
"@
|
|
35
|
-
"@platforma-sdk/model": "1.73.0",
|
|
36
|
-
"@milaboratories/pl-middle-layer": "1.58.1",
|
|
37
|
-
"@milaboratories/ts-builder": "1.3.2"
|
|
37
|
+
"@platforma-sdk/model": "1.75.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@milaboratories/pl-middle-layer": ">=1.
|
|
41
|
-
"@platforma-sdk/model": ">=1.
|
|
40
|
+
"@milaboratories/pl-middle-layer": ">=1.59.1",
|
|
41
|
+
"@platforma-sdk/model": ">=1.75.0"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=22.0.0"
|
package/src/server.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
3
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import type { MiddleLayer } from "@milaboratories/pl-middle-layer";
|
|
6
6
|
import type { Branded } from "@milaboratories/pl-model-common";
|
|
7
7
|
import type { ToolContext } from "./tools/types";
|
|
8
8
|
import { registerPingTool } from "./tools/ping";
|
|
@@ -260,20 +260,20 @@ export class PlMcpServer {
|
|
|
260
260
|
return this.ml;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
/** Resolves a project from the list by its projectId
|
|
263
|
+
/** Resolves a project from the list by its projectId. */
|
|
264
264
|
private async resolveProject(projectId: string) {
|
|
265
265
|
const ml = this.requireMl();
|
|
266
266
|
await ml.projectList.refreshState();
|
|
267
267
|
const projects = await ml.projectList.awaitStableValue();
|
|
268
|
-
const entry = projects.find((p) =>
|
|
268
|
+
const entry = projects.find((p) => p.id === projectId);
|
|
269
269
|
if (!entry) throw new Error(`Project ${projectId} not found`);
|
|
270
270
|
return entry;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
/** Gets an opened project by projectId.
|
|
273
|
+
/** Gets an opened project by projectId. */
|
|
274
274
|
private async getOpenedProject(projectId: string) {
|
|
275
275
|
const ml = this.requireMl();
|
|
276
276
|
const entry = await this.resolveProject(projectId);
|
|
277
|
-
return ml.getOpenedProject(entry.
|
|
277
|
+
return ml.getOpenedProject(entry.id);
|
|
278
278
|
}
|
|
279
279
|
}
|
package/src/tools/projects.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { resourceIdToString } from "@milaboratories/pl-middle-layer";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import type { ToolContext } from "./types";
|
|
5
4
|
import { textResult } from "./types";
|
|
@@ -14,7 +13,7 @@ export function registerProjectTools(server: McpServer, ctx: ToolContext): void
|
|
|
14
13
|
const projects = await ml.projectList.awaitStableValue();
|
|
15
14
|
return textResult(
|
|
16
15
|
projects.map((p) => ({
|
|
17
|
-
projectId:
|
|
16
|
+
projectId: p.id,
|
|
18
17
|
label: p.meta.label,
|
|
19
18
|
opened: p.opened,
|
|
20
19
|
created: p.created.toISOString(),
|
|
@@ -31,8 +30,7 @@ export function registerProjectTools(server: McpServer, ctx: ToolContext): void
|
|
|
31
30
|
inputSchema: { label: z.string().describe("Project name") },
|
|
32
31
|
},
|
|
33
32
|
async ({ label }) => {
|
|
34
|
-
const
|
|
35
|
-
const projectId = resourceIdToString(rid);
|
|
33
|
+
const projectId = await ctx.requireMl().createProject({ label });
|
|
36
34
|
await ctx.callbacks.onProjectCreated?.(projectId);
|
|
37
35
|
return textResult({ projectId });
|
|
38
36
|
},
|
|
@@ -48,7 +46,7 @@ export function registerProjectTools(server: McpServer, ctx: ToolContext): void
|
|
|
48
46
|
},
|
|
49
47
|
async ({ projectId }) => {
|
|
50
48
|
const entry = await ctx.resolveProject(projectId);
|
|
51
|
-
await ctx.requireMl().openProject(entry.
|
|
49
|
+
await ctx.requireMl().openProject(entry.id);
|
|
52
50
|
await ctx.callbacks.onProjectOpened?.(projectId);
|
|
53
51
|
return textResult({ ok: true });
|
|
54
52
|
},
|
|
@@ -62,7 +60,7 @@ export function registerProjectTools(server: McpServer, ctx: ToolContext): void
|
|
|
62
60
|
},
|
|
63
61
|
async ({ projectId }) => {
|
|
64
62
|
const entry = await ctx.resolveProject(projectId);
|
|
65
|
-
await ctx.requireMl().closeProject(entry.
|
|
63
|
+
await ctx.requireMl().closeProject(entry.id);
|
|
66
64
|
await ctx.callbacks.onProjectClosed?.(projectId);
|
|
67
65
|
return textResult({ ok: true });
|
|
68
66
|
},
|