@milaboratories/pl-mcp-server 0.2.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/index.cjs +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/server.cjs +171 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.ts +83 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +171 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/await.cjs +89 -0
- package/dist/tools/await.cjs.map +1 -0
- package/dist/tools/await.js +89 -0
- package/dist/tools/await.js.map +1 -0
- package/dist/tools/block-state.cjs +71 -0
- package/dist/tools/block-state.cjs.map +1 -0
- package/dist/tools/block-state.js +71 -0
- package/dist/tools/block-state.js.map +1 -0
- package/dist/tools/blocks.cjs +123 -0
- package/dist/tools/blocks.cjs.map +1 -0
- package/dist/tools/blocks.js +123 -0
- package/dist/tools/blocks.js.map +1 -0
- package/dist/tools/connection.cjs +33 -0
- package/dist/tools/connection.cjs.map +1 -0
- package/dist/tools/connection.js +33 -0
- package/dist/tools/connection.js.map +1 -0
- package/dist/tools/data-query.cjs +186 -0
- package/dist/tools/data-query.cjs.map +1 -0
- package/dist/tools/data-query.js +186 -0
- package/dist/tools/data-query.js.map +1 -0
- package/dist/tools/logs.cjs +57 -0
- package/dist/tools/logs.cjs.map +1 -0
- package/dist/tools/logs.js +57 -0
- package/dist/tools/logs.js.map +1 -0
- package/dist/tools/ping.cjs +14 -0
- package/dist/tools/ping.cjs.map +1 -0
- package/dist/tools/ping.js +14 -0
- package/dist/tools/ping.js.map +1 -0
- package/dist/tools/projects.cjs +56 -0
- package/dist/tools/projects.cjs.map +1 -0
- package/dist/tools/projects.js +56 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/sandbox.cjs +51 -0
- package/dist/tools/sandbox.cjs.map +1 -0
- package/dist/tools/sandbox.js +51 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/screenshot.cjs +35 -0
- package/dist/tools/screenshot.cjs.map +1 -0
- package/dist/tools/screenshot.js +35 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/tokens.cjs +82 -0
- package/dist/tools/tokens.cjs.map +1 -0
- package/dist/tools/tokens.js +82 -0
- package/dist/tools/tokens.js.map +1 -0
- package/dist/tools/types.cjs +22 -0
- package/dist/tools/types.cjs.map +1 -0
- package/dist/tools/types.js +21 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/ui-interaction.cjs +117 -0
- package/dist/tools/ui-interaction.cjs.map +1 -0
- package/dist/tools/ui-interaction.js +117 -0
- package/dist/tools/ui-interaction.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +7 -0
- package/src/server.ts +271 -0
- package/src/tools/await.ts +151 -0
- package/src/tools/block-state.ts +115 -0
- package/src/tools/blocks.ts +222 -0
- package/src/tools/connection.ts +63 -0
- package/src/tools/data-query.ts +308 -0
- package/src/tools/logs.ts +97 -0
- package/src/tools/ping.ts +9 -0
- package/src/tools/projects.ts +84 -0
- package/src/tools/sandbox.ts +62 -0
- package/src/tools/screenshot.ts +48 -0
- package/src/tools/tokens.test.ts +239 -0
- package/src/tools/tokens.ts +84 -0
- package/src/tools/types.ts +34 -0
- package/src/tools/ui-interaction.ts +156 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type {
|
|
3
|
+
PFrameHandle,
|
|
4
|
+
PTableHandle,
|
|
5
|
+
PTableColumnSpec,
|
|
6
|
+
PTableVector,
|
|
7
|
+
} from "@milaboratories/pl-middle-layer";
|
|
8
|
+
import {
|
|
9
|
+
Annotation,
|
|
10
|
+
pTableValue,
|
|
11
|
+
readAnnotation,
|
|
12
|
+
PFrameDriver,
|
|
13
|
+
} from "@milaboratories/pl-model-common";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import type { ToolContext } from "./types";
|
|
16
|
+
import { safeEval } from "./sandbox";
|
|
17
|
+
import { errorResult, textResult } from "./types";
|
|
18
|
+
|
|
19
|
+
const HEX_HASH_RE = /^[a-f0-9]{64}$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Try to resolve a 64-char hex handle as PTable, then PFrame.
|
|
23
|
+
* Returns a summary object or the original string if neither works.
|
|
24
|
+
*/
|
|
25
|
+
async function resolveHandle(
|
|
26
|
+
handle: string,
|
|
27
|
+
driver: PFrameDriver,
|
|
28
|
+
maxColumns: number,
|
|
29
|
+
cache: Map<string, unknown>,
|
|
30
|
+
): Promise<unknown> {
|
|
31
|
+
if (cache.has(handle)) return cache.get(handle);
|
|
32
|
+
|
|
33
|
+
// Try PTable first (has rows — more useful info)
|
|
34
|
+
try {
|
|
35
|
+
const spec = await driver.getSpec(handle as PTableHandle);
|
|
36
|
+
const shape = await driver.getShape(handle as PTableHandle);
|
|
37
|
+
const summary: Record<string, unknown> = {
|
|
38
|
+
_type: "PTable",
|
|
39
|
+
handle,
|
|
40
|
+
rows: shape.rows,
|
|
41
|
+
columnCount: spec.length,
|
|
42
|
+
columns: spec.slice(0, maxColumns).map((s: PTableColumnSpec, idx: number) => ({
|
|
43
|
+
index: idx,
|
|
44
|
+
type: s.type,
|
|
45
|
+
name: s.spec.name,
|
|
46
|
+
valueType: s.type === "column" ? s.spec.valueType : s.spec.type,
|
|
47
|
+
label: readAnnotation(s.spec, Annotation.Label),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
if (spec.length > maxColumns) {
|
|
51
|
+
summary.truncated = true;
|
|
52
|
+
summary.showing = maxColumns;
|
|
53
|
+
}
|
|
54
|
+
cache.set(handle, summary);
|
|
55
|
+
return summary;
|
|
56
|
+
} catch {
|
|
57
|
+
// not a PTable
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Try PFrame
|
|
61
|
+
try {
|
|
62
|
+
const columns = await driver.listColumns(handle as PFrameHandle);
|
|
63
|
+
const summary: Record<string, unknown> = {
|
|
64
|
+
_type: "PFrame",
|
|
65
|
+
handle,
|
|
66
|
+
columnCount: columns.length,
|
|
67
|
+
columns: columns.slice(0, maxColumns).map((c) => ({
|
|
68
|
+
name: c.spec.name,
|
|
69
|
+
valueType: c.spec.valueType,
|
|
70
|
+
label: readAnnotation(c.spec, Annotation.Label),
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
if (columns.length > maxColumns) {
|
|
74
|
+
summary.truncated = true;
|
|
75
|
+
summary.showing = maxColumns;
|
|
76
|
+
}
|
|
77
|
+
cache.set(handle, summary);
|
|
78
|
+
return summary;
|
|
79
|
+
} catch {
|
|
80
|
+
// not a PFrame either
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cache.set(handle, handle);
|
|
84
|
+
return handle;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively walk a value tree, replacing 64-char hex handles
|
|
89
|
+
* with PFrame/PTable summaries (column specs + row count).
|
|
90
|
+
*/
|
|
91
|
+
async function resolveHandlesInValue(
|
|
92
|
+
value: unknown,
|
|
93
|
+
driver: Parameters<typeof resolveHandle>[1],
|
|
94
|
+
maxColumns: number,
|
|
95
|
+
cache: Map<string, unknown>,
|
|
96
|
+
): Promise<unknown> {
|
|
97
|
+
if (value === null || value === undefined) return value;
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
return HEX_HASH_RE.test(value) ? resolveHandle(value, driver, maxColumns, cache) : value;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value !== "object") return value;
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return Promise.all(value.map((v) => resolveHandlesInValue(v, driver, maxColumns, cache)));
|
|
104
|
+
}
|
|
105
|
+
const out: Record<string, unknown> = {};
|
|
106
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
107
|
+
out[k] = await resolveHandlesInValue(v, driver, maxColumns, cache);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Converts a PTableVector column to a JSON-serializable array using the SDK helper. */
|
|
113
|
+
function vectorToJson(vector: PTableVector, rows: number): (string | number | null)[] {
|
|
114
|
+
const result: (string | number | null)[] = [];
|
|
115
|
+
for (let i = 0; i < rows; i++) {
|
|
116
|
+
result.push(pTableValue(vector, i));
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function registerDataQueryTools(server: McpServer, ctx: ToolContext): void {
|
|
122
|
+
server.registerTool(
|
|
123
|
+
"get_block_outputs",
|
|
124
|
+
{
|
|
125
|
+
description:
|
|
126
|
+
"Get block output values as a JSON map. " +
|
|
127
|
+
"PFrame/PTable handles are resolved inline to summaries with column specs and row counts. " +
|
|
128
|
+
"Use this to discover block results and available data before querying tables.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
projectId: z.string().describe("Project ID"),
|
|
131
|
+
blockId: z.string().describe("Block ID"),
|
|
132
|
+
maxColumns: z
|
|
133
|
+
.number()
|
|
134
|
+
.optional()
|
|
135
|
+
.default(30)
|
|
136
|
+
.describe("Max columns to show per PFrame/PTable summary (default 30)."),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
async ({ projectId, blockId, maxColumns }) => {
|
|
140
|
+
const project = await ctx.getOpenedProject(projectId);
|
|
141
|
+
const state = await project.getBlockState(blockId).getValue();
|
|
142
|
+
if (!state.outputs)
|
|
143
|
+
return errorResult(
|
|
144
|
+
"Block has no outputs yet.",
|
|
145
|
+
"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const outputs = state.outputs as Record<string, { ok?: boolean; value?: unknown }>;
|
|
149
|
+
const driver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
150
|
+
const cache = new Map<string, unknown>();
|
|
151
|
+
|
|
152
|
+
const result: Record<string, unknown> = {};
|
|
153
|
+
for (const [key, output] of Object.entries(outputs)) {
|
|
154
|
+
if (!output?.ok || output.value == null) {
|
|
155
|
+
result[key] = { ok: output?.ok ?? false };
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
result[key] = await resolveHandlesInValue(output.value, driver, maxColumns, cache);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return textResult(result);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
server.registerTool(
|
|
166
|
+
"list_columns",
|
|
167
|
+
{
|
|
168
|
+
description:
|
|
169
|
+
"List all columns in a PFrame with their specs. Use get_block_outputs first to find the PFrame handle.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
pFrameHandle: z
|
|
172
|
+
.string()
|
|
173
|
+
.describe("PFrame handle (64-char hex hash from get_block_outputs)"),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
async ({ pFrameHandle }) => {
|
|
177
|
+
const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
178
|
+
const columns = await pFrameDriver.listColumns(pFrameHandle as PFrameHandle);
|
|
179
|
+
return textResult(
|
|
180
|
+
columns.map((c) => ({
|
|
181
|
+
columnId: c.columnId,
|
|
182
|
+
name: c.spec.name,
|
|
183
|
+
valueType: c.spec.valueType,
|
|
184
|
+
label: readAnnotation(c.spec, Annotation.Label),
|
|
185
|
+
visibility: readAnnotation(c.spec, Annotation.Table.Visibility),
|
|
186
|
+
axes: c.spec.axesSpec.map((a) => ({
|
|
187
|
+
name: a.name,
|
|
188
|
+
type: a.type,
|
|
189
|
+
label: readAnnotation(a, Annotation.Label),
|
|
190
|
+
})),
|
|
191
|
+
})),
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
server.registerTool(
|
|
197
|
+
"query_table",
|
|
198
|
+
{
|
|
199
|
+
description:
|
|
200
|
+
"Query data from a PTable. Returns rows as arrays of values. Use get_block_outputs first to find the PTable handle. " +
|
|
201
|
+
"Use `transform` to process results server-side and return only what you need.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
pTableHandle: z
|
|
204
|
+
.string()
|
|
205
|
+
.describe("PTable handle (64-char hex hash from get_block_outputs)"),
|
|
206
|
+
columns: z
|
|
207
|
+
.array(z.number())
|
|
208
|
+
.optional()
|
|
209
|
+
.describe(
|
|
210
|
+
"Column indices to retrieve (default: all). Use get_block_outputs to see column indices.",
|
|
211
|
+
),
|
|
212
|
+
offset: z.number().optional().default(0).describe("Row offset (default 0)"),
|
|
213
|
+
limit: z.number().optional().default(50).describe("Number of rows to return (default 50)."),
|
|
214
|
+
maxLimit: z
|
|
215
|
+
.number()
|
|
216
|
+
.optional()
|
|
217
|
+
.default(1000)
|
|
218
|
+
.describe("Upper bound for limit (default 1000). Increase for large exports."),
|
|
219
|
+
transform: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
"JS expression evaluated server-side against query results. " +
|
|
224
|
+
"Available variables: `rows` (array of row arrays), `columns` (column headers), `offset`, `rowCount`. " +
|
|
225
|
+
"Example: `rows.map(r => r[0])` — extract first column only.",
|
|
226
|
+
),
|
|
227
|
+
transformTimeout: z
|
|
228
|
+
.number()
|
|
229
|
+
.optional()
|
|
230
|
+
.default(5000)
|
|
231
|
+
.describe("Timeout in ms for transform evaluation (default 5000)."),
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
async ({ pTableHandle, columns, offset, limit, maxLimit, transform, transformTimeout }) => {
|
|
235
|
+
const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
236
|
+
const handle = pTableHandle as PTableHandle;
|
|
237
|
+
|
|
238
|
+
let spec;
|
|
239
|
+
try {
|
|
240
|
+
spec = await pFrameDriver.getSpec(handle);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return textResult({ error: `getSpec failed: ${err}` });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const effectiveLimit = Math.min(limit, maxLimit);
|
|
246
|
+
const range = { offset, length: effectiveLimit };
|
|
247
|
+
|
|
248
|
+
// If no columns specified, get all
|
|
249
|
+
const columnIndices = columns ?? spec.map((_: PTableColumnSpec, i: number) => i);
|
|
250
|
+
|
|
251
|
+
let vectors: PTableVector[];
|
|
252
|
+
try {
|
|
253
|
+
vectors = await pFrameDriver.getData(handle, columnIndices, range);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return textResult({
|
|
256
|
+
error: `getData failed: ${err}`,
|
|
257
|
+
columnIndices,
|
|
258
|
+
range,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const actualRows = vectors.length > 0 ? vectors[0].data.length : 0;
|
|
263
|
+
const columnVectors = vectors.map((v) => vectorToJson(v, actualRows));
|
|
264
|
+
const rows: unknown[][] = [];
|
|
265
|
+
for (let r = 0; r < actualRows; r++) {
|
|
266
|
+
rows.push(columnVectors.map((col) => col[r]));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const columnHeaders = columnIndices.map((idx: number) => {
|
|
270
|
+
const s = spec[idx];
|
|
271
|
+
return {
|
|
272
|
+
index: idx,
|
|
273
|
+
type: s.type,
|
|
274
|
+
name: s.type === "column" ? s.spec.name : s.spec.name,
|
|
275
|
+
label: readAnnotation(s.spec, Annotation.Label),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (transform) {
|
|
280
|
+
try {
|
|
281
|
+
const result = await safeEval(
|
|
282
|
+
transform,
|
|
283
|
+
{
|
|
284
|
+
rows,
|
|
285
|
+
columns: columnHeaders,
|
|
286
|
+
offset,
|
|
287
|
+
rowCount: actualRows,
|
|
288
|
+
},
|
|
289
|
+
transformTimeout,
|
|
290
|
+
);
|
|
291
|
+
return textResult(result);
|
|
292
|
+
} catch (e: unknown) {
|
|
293
|
+
return errorResult(
|
|
294
|
+
`Transform failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
295
|
+
"Check your JS expression syntax. Available variables: rows, columns, offset, rowCount.",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return textResult({
|
|
301
|
+
offset,
|
|
302
|
+
rowCount: actualRows,
|
|
303
|
+
columns: columnHeaders,
|
|
304
|
+
rows,
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { isAnyLogHandle } from "@milaboratories/pl-model-common";
|
|
3
|
+
import type { AnyLogHandle } from "@milaboratories/pl-model-common";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { ToolContext } from "./types";
|
|
6
|
+
import { errorResult, textResult } from "./types";
|
|
7
|
+
|
|
8
|
+
export function registerLogTools(server: McpServer, ctx: ToolContext): void {
|
|
9
|
+
server.registerTool(
|
|
10
|
+
"get_block_logs",
|
|
11
|
+
{
|
|
12
|
+
description:
|
|
13
|
+
"Read execution logs for a block. Extracts log handles from block outputs and reads log content. Returns logs keyed by sample/run ID.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
projectId: z.string().describe("Project ID"),
|
|
16
|
+
blockId: z.string().describe("Block ID"),
|
|
17
|
+
lines: z.number().optional().default(100).describe("Number of lines per log (default 100)"),
|
|
18
|
+
sampleId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Specific sample/key to read logs for (reads all if omitted)"),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async ({ projectId, blockId, lines, sampleId }) => {
|
|
25
|
+
const project = await ctx.getOpenedProject(projectId);
|
|
26
|
+
const state = await project.getBlockState(blockId).getValue();
|
|
27
|
+
if (!state.outputs)
|
|
28
|
+
return errorResult(
|
|
29
|
+
"Block has no outputs yet.",
|
|
30
|
+
"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Scan all outputs for log handles (log+ready:// or log+live://)
|
|
34
|
+
const outputs = state.outputs as Record<
|
|
35
|
+
string,
|
|
36
|
+
{ ok?: boolean; value?: { data?: { key: string[]; value: unknown }[] } }
|
|
37
|
+
>;
|
|
38
|
+
const logEntries: { outputKey: string; key: string[]; handle: AnyLogHandle }[] = [];
|
|
39
|
+
for (const [outputKey, output] of Object.entries(outputs)) {
|
|
40
|
+
if (!output?.ok || !output.value?.data) continue;
|
|
41
|
+
for (const entry of output.value.data) {
|
|
42
|
+
if (isAnyLogHandle(entry.value)) {
|
|
43
|
+
logEntries.push({ outputKey, key: entry.key, handle: entry.value });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (logEntries.length === 0) {
|
|
49
|
+
return errorResult(
|
|
50
|
+
"No log handles found in block outputs.",
|
|
51
|
+
"This block may not produce logs, or it hasn't run yet. Use get_block_outputs to inspect available output types.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const logDriver = ctx.requireMl().driverKit.logDriver;
|
|
56
|
+
const results: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
for (const entry of logEntries) {
|
|
59
|
+
const key = entry.key.join("/");
|
|
60
|
+
if (sampleId && !entry.key.includes(sampleId)) continue;
|
|
61
|
+
try {
|
|
62
|
+
const response = await logDriver.lastLines(entry.handle, lines);
|
|
63
|
+
if (response.shouldUpdateHandle) {
|
|
64
|
+
results[key] = "[log handle stale — block may still be running, retry later]";
|
|
65
|
+
} else {
|
|
66
|
+
results[key] = new TextDecoder().decode(response.data);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
results[key] = `Error reading log: ${err}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return textResult(results);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.registerTool(
|
|
78
|
+
"get_app_log",
|
|
79
|
+
{
|
|
80
|
+
description: "Read recent lines from the application log. Useful for debugging errors.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
lines: z.number().optional().default(50).describe("Number of lines to return (default 50)"),
|
|
83
|
+
search: z.string().optional().describe("Filter lines containing this substring"),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
async ({ lines, search }) => {
|
|
87
|
+
if (!ctx.callbacks.readAppLog) {
|
|
88
|
+
return errorResult(
|
|
89
|
+
"App log reading is not available.",
|
|
90
|
+
"This feature requires the desktop app integration.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const log = await ctx.callbacks.readAppLog(lines, search);
|
|
94
|
+
return textResult({ log });
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ToolContext } from "./types";
|
|
3
|
+
import { textResult } from "./types";
|
|
4
|
+
|
|
5
|
+
export function registerPingTool(server: McpServer, ctx: ToolContext): void {
|
|
6
|
+
server.registerTool("ping", { description: "Health check" }, async () => {
|
|
7
|
+
return textResult({ status: "ok", connected: !!ctx.getMl() });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { resourceIdToString } from "@milaboratories/pl-middle-layer";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { ToolContext } from "./types";
|
|
5
|
+
import { textResult } from "./types";
|
|
6
|
+
|
|
7
|
+
export function registerProjectTools(server: McpServer, ctx: ToolContext): void {
|
|
8
|
+
server.registerTool(
|
|
9
|
+
"list_projects",
|
|
10
|
+
{ description: "List all projects with their IDs, labels, and status" },
|
|
11
|
+
async () => {
|
|
12
|
+
const ml = ctx.requireMl();
|
|
13
|
+
await ml.projectList.refreshState();
|
|
14
|
+
const projects = await ml.projectList.awaitStableValue();
|
|
15
|
+
return textResult(
|
|
16
|
+
projects.map((p) => ({
|
|
17
|
+
projectId: resourceIdToString(p.rid),
|
|
18
|
+
label: p.meta.label,
|
|
19
|
+
opened: p.opened,
|
|
20
|
+
created: p.created.toISOString(),
|
|
21
|
+
lastModified: p.lastModified.toISOString(),
|
|
22
|
+
})),
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
server.registerTool(
|
|
28
|
+
"create_project",
|
|
29
|
+
{
|
|
30
|
+
description: "Create a new project",
|
|
31
|
+
inputSchema: { label: z.string().describe("Project name") },
|
|
32
|
+
},
|
|
33
|
+
async ({ label }) => {
|
|
34
|
+
const rid = await ctx.requireMl().createProject({ label });
|
|
35
|
+
const projectId = resourceIdToString(rid);
|
|
36
|
+
await ctx.callbacks.onProjectCreated?.(projectId);
|
|
37
|
+
return textResult({ projectId });
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
server.registerTool(
|
|
42
|
+
"open_project",
|
|
43
|
+
{
|
|
44
|
+
description: "Open a project for editing. Required before working with blocks.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
projectId: z.string().describe("Project ID from list_projects or create_project"),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
async ({ projectId }) => {
|
|
50
|
+
const entry = await ctx.resolveProject(projectId);
|
|
51
|
+
await ctx.requireMl().openProject(entry.rid);
|
|
52
|
+
await ctx.callbacks.onProjectOpened?.(projectId);
|
|
53
|
+
return textResult({ ok: true });
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
server.registerTool(
|
|
58
|
+
"close_project",
|
|
59
|
+
{
|
|
60
|
+
description: "Close an opened project, releasing its resources",
|
|
61
|
+
inputSchema: { projectId: z.string().describe("Project ID") },
|
|
62
|
+
},
|
|
63
|
+
async ({ projectId }) => {
|
|
64
|
+
const entry = await ctx.resolveProject(projectId);
|
|
65
|
+
await ctx.requireMl().closeProject(entry.rid);
|
|
66
|
+
await ctx.callbacks.onProjectClosed?.(projectId);
|
|
67
|
+
return textResult({ ok: true });
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
server.registerTool(
|
|
72
|
+
"delete_project",
|
|
73
|
+
{
|
|
74
|
+
description: "Delete a project permanently. The project must be closed first.",
|
|
75
|
+
inputSchema: { projectId: z.string().describe("Project ID") },
|
|
76
|
+
},
|
|
77
|
+
async ({ projectId }) => {
|
|
78
|
+
const entry = await ctx.resolveProject(projectId);
|
|
79
|
+
await ctx.requireMl().deleteProject(entry.id);
|
|
80
|
+
await ctx.callbacks.onProjectDeleted?.(projectId);
|
|
81
|
+
return textResult({ ok: true });
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getQuickJS, type QuickJSRuntime } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
/** Lazily initialized QuickJS runtime shared across evaluations. */
|
|
4
|
+
let sharedRuntime: QuickJSRuntime | undefined;
|
|
5
|
+
|
|
6
|
+
async function getRuntime(): Promise<QuickJSRuntime> {
|
|
7
|
+
if (!sharedRuntime) {
|
|
8
|
+
const quickjs = await getQuickJS();
|
|
9
|
+
sharedRuntime = quickjs.newRuntime();
|
|
10
|
+
sharedRuntime.setMemoryLimit(1024 * 1024 * 16); // 16 MB
|
|
11
|
+
sharedRuntime.setMaxStackSize(1024 * 320);
|
|
12
|
+
}
|
|
13
|
+
return sharedRuntime;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Evaluate a JS expression in a QuickJS sandbox.
|
|
18
|
+
* Variables from `context` are injected as globals.
|
|
19
|
+
* Data is marshaled via JSON — no access to Node APIs, filesystem, or process.
|
|
20
|
+
*/
|
|
21
|
+
export async function safeEval(
|
|
22
|
+
expression: string,
|
|
23
|
+
context: Record<string, unknown>,
|
|
24
|
+
timeout: number,
|
|
25
|
+
): Promise<unknown> {
|
|
26
|
+
const runtime = await getRuntime();
|
|
27
|
+
|
|
28
|
+
// Set interrupt handler for timeout
|
|
29
|
+
const deadline = Date.now() + timeout;
|
|
30
|
+
runtime.setInterruptHandler(() => Date.now() > deadline);
|
|
31
|
+
|
|
32
|
+
const vm = runtime.newContext();
|
|
33
|
+
try {
|
|
34
|
+
// Inject context variables via JSON
|
|
35
|
+
const contextJson = JSON.stringify(context);
|
|
36
|
+
const setup = `const __ctx = JSON.parse(${JSON.stringify(contextJson)});
|
|
37
|
+
${Object.keys(context)
|
|
38
|
+
.map((k) => `const ${k} = __ctx[${JSON.stringify(k)}];`)
|
|
39
|
+
.join("\n")}`;
|
|
40
|
+
const setupResult = vm.evalCode(setup, "setup.js", { type: "global" });
|
|
41
|
+
if (setupResult.error) {
|
|
42
|
+
const err = vm.dump(setupResult.error);
|
|
43
|
+
setupResult.error.dispose();
|
|
44
|
+
throw new Error(`Context setup failed: ${err}`);
|
|
45
|
+
}
|
|
46
|
+
setupResult.value.dispose();
|
|
47
|
+
|
|
48
|
+
// Evaluate the expression
|
|
49
|
+
const result = vm.evalCode(`JSON.stringify((${expression}))`, "transform.js");
|
|
50
|
+
if (result.error) {
|
|
51
|
+
const err = vm.dump(result.error);
|
|
52
|
+
result.error.dispose();
|
|
53
|
+
throw new Error(String(err));
|
|
54
|
+
}
|
|
55
|
+
const json = vm.getString(result.value);
|
|
56
|
+
result.value.dispose();
|
|
57
|
+
return JSON.parse(json);
|
|
58
|
+
} finally {
|
|
59
|
+
runtime.setInterruptHandler(() => false);
|
|
60
|
+
vm.dispose();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { ToolContext } from "./types";
|
|
6
|
+
import { errorResult } from "./types";
|
|
7
|
+
|
|
8
|
+
export function registerScreenshotTool(server: McpServer, ctx: ToolContext): void {
|
|
9
|
+
server.registerTool(
|
|
10
|
+
"capture_screenshot",
|
|
11
|
+
{
|
|
12
|
+
description:
|
|
13
|
+
"Capture a screenshot of the current application window. Optionally save to a file.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
savePath: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe(
|
|
19
|
+
"Absolute file path to save the screenshot as PNG. If omitted, returns the image inline only.",
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
async ({ savePath }: { savePath?: string }) => {
|
|
24
|
+
if (!ctx.callbacks.captureScreenshot) {
|
|
25
|
+
return errorResult(
|
|
26
|
+
"Screenshot capture is not available.",
|
|
27
|
+
"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",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const base64Png = await ctx.callbacks.captureScreenshot();
|
|
31
|
+
|
|
32
|
+
if (savePath) {
|
|
33
|
+
const absPath = resolve(savePath);
|
|
34
|
+
await writeFile(absPath, Buffer.from(base64Png, "base64"), { flag: "wx" });
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{ type: "image" as const, data: base64Png, mimeType: "image/png" },
|
|
38
|
+
{ type: "text" as const, text: `Screenshot saved to ${absPath}` },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "image" as const, data: base64Png, mimeType: "image/png" }],
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
}
|