@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,33 @@
|
|
|
1
|
+
import { errorResult, textResult } from "./types.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/tools/connection.ts
|
|
4
|
+
function registerConnectionTools(server, ctx) {
|
|
5
|
+
server.registerTool("get_connection_status", { description: "Get current server connection status" }, async () => {
|
|
6
|
+
if (!ctx.callbacks.getConnectionStatus) return textResult({ connected: !!ctx.getMl() });
|
|
7
|
+
return textResult(await ctx.callbacks.getConnectionStatus());
|
|
8
|
+
});
|
|
9
|
+
server.registerTool("list_connections", { description: "List saved server connections" }, async () => {
|
|
10
|
+
if (!ctx.callbacks.listConnections) return errorResult("Connection management is not available.", "The desktop app integration may not support this feature.");
|
|
11
|
+
return textResult(await ctx.callbacks.listConnections());
|
|
12
|
+
});
|
|
13
|
+
server.registerTool("connect_to_server", {
|
|
14
|
+
description: "Connect to a Platforma server. Use list_connections to see saved servers.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
addr: z.string().describe("Server address (e.g. https://pl6.demo2.platforma.bio:6346)"),
|
|
17
|
+
login: z.string().describe("Username"),
|
|
18
|
+
password: z.string().optional().describe("Password (uses saved token if omitted)")
|
|
19
|
+
}
|
|
20
|
+
}, async ({ addr, login, password }) => {
|
|
21
|
+
if (!ctx.callbacks.connectToServer) return errorResult("Failed to connect to Platforma Server.", "Check that provided URL is available and accepts connecitons.");
|
|
22
|
+
return textResult(await ctx.callbacks.connectToServer(addr, login, password));
|
|
23
|
+
});
|
|
24
|
+
server.registerTool("disconnect", { description: "Disconnect from current server" }, async () => {
|
|
25
|
+
if (!ctx.callbacks.disconnect) return errorResult("Failed to disconnect.", "More likely it's because connection is already closed. Could check it with 'get_connection_status' tool.");
|
|
26
|
+
await ctx.callbacks.disconnect();
|
|
27
|
+
return textResult({ ok: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { registerConnectionTools };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","names":[],"sources":["../../src/tools/connection.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 registerConnectionTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_connection_status\",\n { description: \"Get current server connection status\" },\n async () => {\n if (!ctx.callbacks.getConnectionStatus) {\n return textResult({ connected: !!ctx.getMl() });\n }\n return textResult(await ctx.callbacks.getConnectionStatus());\n },\n );\n\n server.registerTool(\n \"list_connections\",\n { description: \"List saved server connections\" },\n async () => {\n if (!ctx.callbacks.listConnections) {\n return errorResult(\n \"Connection management is not available.\",\n \"The desktop app integration may not support this feature.\",\n );\n }\n return textResult(await ctx.callbacks.listConnections());\n },\n );\n\n server.registerTool(\n \"connect_to_server\",\n {\n description: \"Connect to a Platforma server. Use list_connections to see saved servers.\",\n inputSchema: {\n addr: z.string().describe(\"Server address (e.g. https://pl6.demo2.platforma.bio:6346)\"),\n login: z.string().describe(\"Username\"),\n password: z.string().optional().describe(\"Password (uses saved token if omitted)\"),\n },\n },\n async ({ addr, login, password }) => {\n if (!ctx.callbacks.connectToServer) {\n return errorResult(\n \"Failed to connect to Platforma Server.\",\n \"Check that provided URL is available and accepts connecitons.\",\n );\n }\n return textResult(await ctx.callbacks.connectToServer(addr, login, password));\n },\n );\n\n server.registerTool(\"disconnect\", { description: \"Disconnect from current server\" }, async () => {\n if (!ctx.callbacks.disconnect) {\n return errorResult(\n \"Failed to disconnect.\",\n \"More likely it's because connection is already closed. Could check it with 'get_connection_status' tool.\",\n );\n }\n await ctx.callbacks.disconnect();\n return textResult({ ok: true });\n });\n}\n"],"mappings":";;;AAKA,SAAgB,wBAAwB,QAAmB,KAAwB;AACjF,QAAO,aACL,yBACA,EAAE,aAAa,wCAAwC,EACvD,YAAY;AACV,MAAI,CAAC,IAAI,UAAU,oBACjB,QAAO,WAAW,EAAE,WAAW,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;AAEjD,SAAO,WAAW,MAAM,IAAI,UAAU,qBAAqB,CAAC;GAE/D;AAED,QAAO,aACL,oBACA,EAAE,aAAa,iCAAiC,EAChD,YAAY;AACV,MAAI,CAAC,IAAI,UAAU,gBACjB,QAAO,YACL,2CACA,4DACD;AAEH,SAAO,WAAW,MAAM,IAAI,UAAU,iBAAiB,CAAC;GAE3D;AAED,QAAO,aACL,qBACA;EACE,aAAa;EACb,aAAa;GACX,MAAM,EAAE,QAAQ,CAAC,SAAS,6DAA6D;GACvF,OAAO,EAAE,QAAQ,CAAC,SAAS,WAAW;GACtC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,yCAAyC;GACnF;EACF,EACD,OAAO,EAAE,MAAM,OAAO,eAAe;AACnC,MAAI,CAAC,IAAI,UAAU,gBACjB,QAAO,YACL,0CACA,gEACD;AAEH,SAAO,WAAW,MAAM,IAAI,UAAU,gBAAgB,MAAM,OAAO,SAAS,CAAC;GAEhF;AAED,QAAO,aAAa,cAAc,EAAE,aAAa,kCAAkC,EAAE,YAAY;AAC/F,MAAI,CAAC,IAAI,UAAU,WACjB,QAAO,YACL,yBACA,2GACD;AAEH,QAAM,IAAI,UAAU,YAAY;AAChC,SAAO,WAAW,EAAE,IAAI,MAAM,CAAC;GAC/B"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const require_types = require("./types.cjs");
|
|
2
|
+
const require_sandbox = require("./sandbox.cjs");
|
|
3
|
+
let zod = require("zod");
|
|
4
|
+
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
5
|
+
//#region src/tools/data-query.ts
|
|
6
|
+
const HEX_HASH_RE = /^[a-f0-9]{64}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Try to resolve a 64-char hex handle as PTable, then PFrame.
|
|
9
|
+
* Returns a summary object or the original string if neither works.
|
|
10
|
+
*/
|
|
11
|
+
async function resolveHandle(handle, driver, maxColumns, cache) {
|
|
12
|
+
if (cache.has(handle)) return cache.get(handle);
|
|
13
|
+
try {
|
|
14
|
+
const spec = await driver.getSpec(handle);
|
|
15
|
+
const summary = {
|
|
16
|
+
_type: "PTable",
|
|
17
|
+
handle,
|
|
18
|
+
rows: (await driver.getShape(handle)).rows,
|
|
19
|
+
columnCount: spec.length,
|
|
20
|
+
columns: spec.slice(0, maxColumns).map((s, idx) => ({
|
|
21
|
+
index: idx,
|
|
22
|
+
type: s.type,
|
|
23
|
+
name: s.spec.name,
|
|
24
|
+
valueType: s.type === "column" ? s.spec.valueType : s.spec.type,
|
|
25
|
+
label: (0, _milaboratories_pl_model_common.readAnnotation)(s.spec, _milaboratories_pl_model_common.Annotation.Label)
|
|
26
|
+
}))
|
|
27
|
+
};
|
|
28
|
+
if (spec.length > maxColumns) {
|
|
29
|
+
summary.truncated = true;
|
|
30
|
+
summary.showing = maxColumns;
|
|
31
|
+
}
|
|
32
|
+
cache.set(handle, summary);
|
|
33
|
+
return summary;
|
|
34
|
+
} catch {}
|
|
35
|
+
try {
|
|
36
|
+
const columns = await driver.listColumns(handle);
|
|
37
|
+
const summary = {
|
|
38
|
+
_type: "PFrame",
|
|
39
|
+
handle,
|
|
40
|
+
columnCount: columns.length,
|
|
41
|
+
columns: columns.slice(0, maxColumns).map((c) => ({
|
|
42
|
+
name: c.spec.name,
|
|
43
|
+
valueType: c.spec.valueType,
|
|
44
|
+
label: (0, _milaboratories_pl_model_common.readAnnotation)(c.spec, _milaboratories_pl_model_common.Annotation.Label)
|
|
45
|
+
}))
|
|
46
|
+
};
|
|
47
|
+
if (columns.length > maxColumns) {
|
|
48
|
+
summary.truncated = true;
|
|
49
|
+
summary.showing = maxColumns;
|
|
50
|
+
}
|
|
51
|
+
cache.set(handle, summary);
|
|
52
|
+
return summary;
|
|
53
|
+
} catch {}
|
|
54
|
+
cache.set(handle, handle);
|
|
55
|
+
return handle;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively walk a value tree, replacing 64-char hex handles
|
|
59
|
+
* with PFrame/PTable summaries (column specs + row count).
|
|
60
|
+
*/
|
|
61
|
+
async function resolveHandlesInValue(value, driver, maxColumns, cache) {
|
|
62
|
+
if (value === null || value === void 0) return value;
|
|
63
|
+
if (typeof value === "string") return HEX_HASH_RE.test(value) ? resolveHandle(value, driver, maxColumns, cache) : value;
|
|
64
|
+
if (typeof value !== "object") return value;
|
|
65
|
+
if (Array.isArray(value)) return Promise.all(value.map((v) => resolveHandlesInValue(v, driver, maxColumns, cache)));
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(value)) out[k] = await resolveHandlesInValue(v, driver, maxColumns, cache);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** Converts a PTableVector column to a JSON-serializable array using the SDK helper. */
|
|
71
|
+
function vectorToJson(vector, rows) {
|
|
72
|
+
const result = [];
|
|
73
|
+
for (let i = 0; i < rows; i++) result.push((0, _milaboratories_pl_model_common.pTableValue)(vector, i));
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function registerDataQueryTools(server, ctx) {
|
|
77
|
+
server.registerTool("get_block_outputs", {
|
|
78
|
+
description: "Get block output values as a JSON map. PFrame/PTable handles are resolved inline to summaries with column specs and row counts. Use this to discover block results and available data before querying tables.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
projectId: zod.z.string().describe("Project ID"),
|
|
81
|
+
blockId: zod.z.string().describe("Block ID"),
|
|
82
|
+
maxColumns: zod.z.number().optional().default(30).describe("Max columns to show per PFrame/PTable summary (default 30).")
|
|
83
|
+
}
|
|
84
|
+
}, async ({ projectId, blockId, maxColumns }) => {
|
|
85
|
+
const state = await (await ctx.getOpenedProject(projectId)).getBlockState(blockId).getValue();
|
|
86
|
+
if (!state.outputs) return require_types.errorResult("Block has no outputs yet.", "The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.");
|
|
87
|
+
const outputs = state.outputs;
|
|
88
|
+
const driver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
89
|
+
const cache = /* @__PURE__ */ new Map();
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [key, output] of Object.entries(outputs)) {
|
|
92
|
+
if (!output?.ok || output.value == null) {
|
|
93
|
+
result[key] = { ok: output?.ok ?? false };
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
result[key] = await resolveHandlesInValue(output.value, driver, maxColumns, cache);
|
|
97
|
+
}
|
|
98
|
+
return require_types.textResult(result);
|
|
99
|
+
});
|
|
100
|
+
server.registerTool("list_columns", {
|
|
101
|
+
description: "List all columns in a PFrame with their specs. Use get_block_outputs first to find the PFrame handle.",
|
|
102
|
+
inputSchema: { pFrameHandle: zod.z.string().describe("PFrame handle (64-char hex hash from get_block_outputs)") }
|
|
103
|
+
}, async ({ pFrameHandle }) => {
|
|
104
|
+
return require_types.textResult((await ctx.requireMl().internalDriverKit.pFrameDriver.listColumns(pFrameHandle)).map((c) => ({
|
|
105
|
+
columnId: c.columnId,
|
|
106
|
+
name: c.spec.name,
|
|
107
|
+
valueType: c.spec.valueType,
|
|
108
|
+
label: (0, _milaboratories_pl_model_common.readAnnotation)(c.spec, _milaboratories_pl_model_common.Annotation.Label),
|
|
109
|
+
visibility: (0, _milaboratories_pl_model_common.readAnnotation)(c.spec, _milaboratories_pl_model_common.Annotation.Table.Visibility),
|
|
110
|
+
axes: c.spec.axesSpec.map((a) => ({
|
|
111
|
+
name: a.name,
|
|
112
|
+
type: a.type,
|
|
113
|
+
label: (0, _milaboratories_pl_model_common.readAnnotation)(a, _milaboratories_pl_model_common.Annotation.Label)
|
|
114
|
+
}))
|
|
115
|
+
})));
|
|
116
|
+
});
|
|
117
|
+
server.registerTool("query_table", {
|
|
118
|
+
description: "Query data from a PTable. Returns rows as arrays of values. Use get_block_outputs first to find the PTable handle. Use `transform` to process results server-side and return only what you need.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
pTableHandle: zod.z.string().describe("PTable handle (64-char hex hash from get_block_outputs)"),
|
|
121
|
+
columns: zod.z.array(zod.z.number()).optional().describe("Column indices to retrieve (default: all). Use get_block_outputs to see column indices."),
|
|
122
|
+
offset: zod.z.number().optional().default(0).describe("Row offset (default 0)"),
|
|
123
|
+
limit: zod.z.number().optional().default(50).describe("Number of rows to return (default 50)."),
|
|
124
|
+
maxLimit: zod.z.number().optional().default(1e3).describe("Upper bound for limit (default 1000). Increase for large exports."),
|
|
125
|
+
transform: zod.z.string().optional().describe("JS expression evaluated server-side against query results. Available variables: `rows` (array of row arrays), `columns` (column headers), `offset`, `rowCount`. Example: `rows.map(r => r[0])` — extract first column only."),
|
|
126
|
+
transformTimeout: zod.z.number().optional().default(5e3).describe("Timeout in ms for transform evaluation (default 5000).")
|
|
127
|
+
}
|
|
128
|
+
}, async ({ pTableHandle, columns, offset, limit, maxLimit, transform, transformTimeout }) => {
|
|
129
|
+
const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
130
|
+
const handle = pTableHandle;
|
|
131
|
+
let spec;
|
|
132
|
+
try {
|
|
133
|
+
spec = await pFrameDriver.getSpec(handle);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return require_types.textResult({ error: `getSpec failed: ${err}` });
|
|
136
|
+
}
|
|
137
|
+
const range = {
|
|
138
|
+
offset,
|
|
139
|
+
length: Math.min(limit, maxLimit)
|
|
140
|
+
};
|
|
141
|
+
const columnIndices = columns ?? spec.map((_, i) => i);
|
|
142
|
+
let vectors;
|
|
143
|
+
try {
|
|
144
|
+
vectors = await pFrameDriver.getData(handle, columnIndices, range);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return require_types.textResult({
|
|
147
|
+
error: `getData failed: ${err}`,
|
|
148
|
+
columnIndices,
|
|
149
|
+
range
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const actualRows = vectors.length > 0 ? vectors[0].data.length : 0;
|
|
153
|
+
const columnVectors = vectors.map((v) => vectorToJson(v, actualRows));
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (let r = 0; r < actualRows; r++) rows.push(columnVectors.map((col) => col[r]));
|
|
156
|
+
const columnHeaders = columnIndices.map((idx) => {
|
|
157
|
+
const s = spec[idx];
|
|
158
|
+
return {
|
|
159
|
+
index: idx,
|
|
160
|
+
type: s.type,
|
|
161
|
+
name: s.type === "column" ? s.spec.name : s.spec.name,
|
|
162
|
+
label: (0, _milaboratories_pl_model_common.readAnnotation)(s.spec, _milaboratories_pl_model_common.Annotation.Label)
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
if (transform) try {
|
|
166
|
+
return require_types.textResult(await require_sandbox.safeEval(transform, {
|
|
167
|
+
rows,
|
|
168
|
+
columns: columnHeaders,
|
|
169
|
+
offset,
|
|
170
|
+
rowCount: actualRows
|
|
171
|
+
}, transformTimeout));
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return require_types.errorResult(`Transform failed: ${e instanceof Error ? e.message : String(e)}`, "Check your JS expression syntax. Available variables: rows, columns, offset, rowCount.");
|
|
174
|
+
}
|
|
175
|
+
return require_types.textResult({
|
|
176
|
+
offset,
|
|
177
|
+
rowCount: actualRows,
|
|
178
|
+
columns: columnHeaders,
|
|
179
|
+
rows
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
exports.registerDataQueryTools = registerDataQueryTools;
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=data-query.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-query.cjs","names":["Annotation","z","errorResult","textResult","safeEval"],"sources":["../../src/tools/data-query.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type {\n PFrameHandle,\n PTableHandle,\n PTableColumnSpec,\n PTableVector,\n} from \"@milaboratories/pl-middle-layer\";\nimport {\n Annotation,\n pTableValue,\n readAnnotation,\n PFrameDriver,\n} from \"@milaboratories/pl-model-common\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nconst HEX_HASH_RE = /^[a-f0-9]{64}$/;\n\n/**\n * Try to resolve a 64-char hex handle as PTable, then PFrame.\n * Returns a summary object or the original string if neither works.\n */\nasync function resolveHandle(\n handle: string,\n driver: PFrameDriver,\n maxColumns: number,\n cache: Map<string, unknown>,\n): Promise<unknown> {\n if (cache.has(handle)) return cache.get(handle);\n\n // Try PTable first (has rows — more useful info)\n try {\n const spec = await driver.getSpec(handle as PTableHandle);\n const shape = await driver.getShape(handle as PTableHandle);\n const summary: Record<string, unknown> = {\n _type: \"PTable\",\n handle,\n rows: shape.rows,\n columnCount: spec.length,\n columns: spec.slice(0, maxColumns).map((s: PTableColumnSpec, idx: number) => ({\n index: idx,\n type: s.type,\n name: s.spec.name,\n valueType: s.type === \"column\" ? s.spec.valueType : s.spec.type,\n label: readAnnotation(s.spec, Annotation.Label),\n })),\n };\n if (spec.length > maxColumns) {\n summary.truncated = true;\n summary.showing = maxColumns;\n }\n cache.set(handle, summary);\n return summary;\n } catch {\n // not a PTable\n }\n\n // Try PFrame\n try {\n const columns = await driver.listColumns(handle as PFrameHandle);\n const summary: Record<string, unknown> = {\n _type: \"PFrame\",\n handle,\n columnCount: columns.length,\n columns: columns.slice(0, maxColumns).map((c) => ({\n name: c.spec.name,\n valueType: c.spec.valueType,\n label: readAnnotation(c.spec, Annotation.Label),\n })),\n };\n if (columns.length > maxColumns) {\n summary.truncated = true;\n summary.showing = maxColumns;\n }\n cache.set(handle, summary);\n return summary;\n } catch {\n // not a PFrame either\n }\n\n cache.set(handle, handle);\n return handle;\n}\n\n/**\n * Recursively walk a value tree, replacing 64-char hex handles\n * with PFrame/PTable summaries (column specs + row count).\n */\nasync function resolveHandlesInValue(\n value: unknown,\n driver: Parameters<typeof resolveHandle>[1],\n maxColumns: number,\n cache: Map<string, unknown>,\n): Promise<unknown> {\n if (value === null || value === undefined) return value;\n if (typeof value === \"string\") {\n return HEX_HASH_RE.test(value) ? resolveHandle(value, driver, maxColumns, cache) : value;\n }\n if (typeof value !== \"object\") return value;\n if (Array.isArray(value)) {\n return Promise.all(value.map((v) => resolveHandlesInValue(v, driver, maxColumns, cache)));\n }\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = await resolveHandlesInValue(v, driver, maxColumns, cache);\n }\n return out;\n}\n\n/** Converts a PTableVector column to a JSON-serializable array using the SDK helper. */\nfunction vectorToJson(vector: PTableVector, rows: number): (string | number | null)[] {\n const result: (string | number | null)[] = [];\n for (let i = 0; i < rows; i++) {\n result.push(pTableValue(vector, i));\n }\n return result;\n}\n\nexport function registerDataQueryTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_block_outputs\",\n {\n description:\n \"Get block output values as a JSON map. \" +\n \"PFrame/PTable handles are resolved inline to summaries with column specs and row counts. \" +\n \"Use this to discover block results and available data before querying tables.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n maxColumns: z\n .number()\n .optional()\n .default(30)\n .describe(\"Max columns to show per PFrame/PTable summary (default 30).\"),\n },\n },\n async ({ projectId, blockId, maxColumns }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n if (!state.outputs)\n return errorResult(\n \"Block has no outputs yet.\",\n \"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.\",\n );\n\n const outputs = state.outputs as Record<string, { ok?: boolean; value?: unknown }>;\n const driver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const cache = new Map<string, unknown>();\n\n const result: Record<string, unknown> = {};\n for (const [key, output] of Object.entries(outputs)) {\n if (!output?.ok || output.value == null) {\n result[key] = { ok: output?.ok ?? false };\n continue;\n }\n result[key] = await resolveHandlesInValue(output.value, driver, maxColumns, cache);\n }\n\n return textResult(result);\n },\n );\n\n server.registerTool(\n \"list_columns\",\n {\n description:\n \"List all columns in a PFrame with their specs. Use get_block_outputs first to find the PFrame handle.\",\n inputSchema: {\n pFrameHandle: z\n .string()\n .describe(\"PFrame handle (64-char hex hash from get_block_outputs)\"),\n },\n },\n async ({ pFrameHandle }) => {\n const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const columns = await pFrameDriver.listColumns(pFrameHandle as PFrameHandle);\n return textResult(\n columns.map((c) => ({\n columnId: c.columnId,\n name: c.spec.name,\n valueType: c.spec.valueType,\n label: readAnnotation(c.spec, Annotation.Label),\n visibility: readAnnotation(c.spec, Annotation.Table.Visibility),\n axes: c.spec.axesSpec.map((a) => ({\n name: a.name,\n type: a.type,\n label: readAnnotation(a, Annotation.Label),\n })),\n })),\n );\n },\n );\n\n server.registerTool(\n \"query_table\",\n {\n description:\n \"Query data from a PTable. Returns rows as arrays of values. Use get_block_outputs first to find the PTable handle. \" +\n \"Use `transform` to process results server-side and return only what you need.\",\n inputSchema: {\n pTableHandle: z\n .string()\n .describe(\"PTable handle (64-char hex hash from get_block_outputs)\"),\n columns: z\n .array(z.number())\n .optional()\n .describe(\n \"Column indices to retrieve (default: all). Use get_block_outputs to see column indices.\",\n ),\n offset: z.number().optional().default(0).describe(\"Row offset (default 0)\"),\n limit: z.number().optional().default(50).describe(\"Number of rows to return (default 50).\"),\n maxLimit: z\n .number()\n .optional()\n .default(1000)\n .describe(\"Upper bound for limit (default 1000). Increase for large exports.\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against query results. \" +\n \"Available variables: `rows` (array of row arrays), `columns` (column headers), `offset`, `rowCount`. \" +\n \"Example: `rows.map(r => r[0])` — extract first column only.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ pTableHandle, columns, offset, limit, maxLimit, transform, transformTimeout }) => {\n const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const handle = pTableHandle as PTableHandle;\n\n let spec;\n try {\n spec = await pFrameDriver.getSpec(handle);\n } catch (err) {\n return textResult({ error: `getSpec failed: ${err}` });\n }\n\n const effectiveLimit = Math.min(limit, maxLimit);\n const range = { offset, length: effectiveLimit };\n\n // If no columns specified, get all\n const columnIndices = columns ?? spec.map((_: PTableColumnSpec, i: number) => i);\n\n let vectors: PTableVector[];\n try {\n vectors = await pFrameDriver.getData(handle, columnIndices, range);\n } catch (err) {\n return textResult({\n error: `getData failed: ${err}`,\n columnIndices,\n range,\n });\n }\n\n const actualRows = vectors.length > 0 ? vectors[0].data.length : 0;\n const columnVectors = vectors.map((v) => vectorToJson(v, actualRows));\n const rows: unknown[][] = [];\n for (let r = 0; r < actualRows; r++) {\n rows.push(columnVectors.map((col) => col[r]));\n }\n\n const columnHeaders = columnIndices.map((idx: number) => {\n const s = spec[idx];\n return {\n index: idx,\n type: s.type,\n name: s.type === \"column\" ? s.spec.name : s.spec.name,\n label: readAnnotation(s.spec, Annotation.Label),\n };\n });\n\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n {\n rows,\n columns: columnHeaders,\n offset,\n rowCount: actualRows,\n },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: rows, columns, offset, rowCount.\",\n );\n }\n }\n\n return textResult({\n offset,\n rowCount: actualRows,\n columns: columnHeaders,\n rows,\n });\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { errorResult, textResult } from "./types.js";
|
|
2
|
+
import { safeEval } from "./sandbox.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Annotation, pTableValue, readAnnotation } from "@milaboratories/pl-model-common";
|
|
5
|
+
//#region src/tools/data-query.ts
|
|
6
|
+
const HEX_HASH_RE = /^[a-f0-9]{64}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Try to resolve a 64-char hex handle as PTable, then PFrame.
|
|
9
|
+
* Returns a summary object or the original string if neither works.
|
|
10
|
+
*/
|
|
11
|
+
async function resolveHandle(handle, driver, maxColumns, cache) {
|
|
12
|
+
if (cache.has(handle)) return cache.get(handle);
|
|
13
|
+
try {
|
|
14
|
+
const spec = await driver.getSpec(handle);
|
|
15
|
+
const summary = {
|
|
16
|
+
_type: "PTable",
|
|
17
|
+
handle,
|
|
18
|
+
rows: (await driver.getShape(handle)).rows,
|
|
19
|
+
columnCount: spec.length,
|
|
20
|
+
columns: spec.slice(0, maxColumns).map((s, idx) => ({
|
|
21
|
+
index: idx,
|
|
22
|
+
type: s.type,
|
|
23
|
+
name: s.spec.name,
|
|
24
|
+
valueType: s.type === "column" ? s.spec.valueType : s.spec.type,
|
|
25
|
+
label: readAnnotation(s.spec, Annotation.Label)
|
|
26
|
+
}))
|
|
27
|
+
};
|
|
28
|
+
if (spec.length > maxColumns) {
|
|
29
|
+
summary.truncated = true;
|
|
30
|
+
summary.showing = maxColumns;
|
|
31
|
+
}
|
|
32
|
+
cache.set(handle, summary);
|
|
33
|
+
return summary;
|
|
34
|
+
} catch {}
|
|
35
|
+
try {
|
|
36
|
+
const columns = await driver.listColumns(handle);
|
|
37
|
+
const summary = {
|
|
38
|
+
_type: "PFrame",
|
|
39
|
+
handle,
|
|
40
|
+
columnCount: columns.length,
|
|
41
|
+
columns: columns.slice(0, maxColumns).map((c) => ({
|
|
42
|
+
name: c.spec.name,
|
|
43
|
+
valueType: c.spec.valueType,
|
|
44
|
+
label: readAnnotation(c.spec, Annotation.Label)
|
|
45
|
+
}))
|
|
46
|
+
};
|
|
47
|
+
if (columns.length > maxColumns) {
|
|
48
|
+
summary.truncated = true;
|
|
49
|
+
summary.showing = maxColumns;
|
|
50
|
+
}
|
|
51
|
+
cache.set(handle, summary);
|
|
52
|
+
return summary;
|
|
53
|
+
} catch {}
|
|
54
|
+
cache.set(handle, handle);
|
|
55
|
+
return handle;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively walk a value tree, replacing 64-char hex handles
|
|
59
|
+
* with PFrame/PTable summaries (column specs + row count).
|
|
60
|
+
*/
|
|
61
|
+
async function resolveHandlesInValue(value, driver, maxColumns, cache) {
|
|
62
|
+
if (value === null || value === void 0) return value;
|
|
63
|
+
if (typeof value === "string") return HEX_HASH_RE.test(value) ? resolveHandle(value, driver, maxColumns, cache) : value;
|
|
64
|
+
if (typeof value !== "object") return value;
|
|
65
|
+
if (Array.isArray(value)) return Promise.all(value.map((v) => resolveHandlesInValue(v, driver, maxColumns, cache)));
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(value)) out[k] = await resolveHandlesInValue(v, driver, maxColumns, cache);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** Converts a PTableVector column to a JSON-serializable array using the SDK helper. */
|
|
71
|
+
function vectorToJson(vector, rows) {
|
|
72
|
+
const result = [];
|
|
73
|
+
for (let i = 0; i < rows; i++) result.push(pTableValue(vector, i));
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function registerDataQueryTools(server, ctx) {
|
|
77
|
+
server.registerTool("get_block_outputs", {
|
|
78
|
+
description: "Get block output values as a JSON map. PFrame/PTable handles are resolved inline to summaries with column specs and row counts. Use this to discover block results and available data before querying tables.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
projectId: z.string().describe("Project ID"),
|
|
81
|
+
blockId: z.string().describe("Block ID"),
|
|
82
|
+
maxColumns: z.number().optional().default(30).describe("Max columns to show per PFrame/PTable summary (default 30).")
|
|
83
|
+
}
|
|
84
|
+
}, async ({ projectId, blockId, maxColumns }) => {
|
|
85
|
+
const state = await (await ctx.getOpenedProject(projectId)).getBlockState(blockId).getValue();
|
|
86
|
+
if (!state.outputs) return errorResult("Block has no outputs yet.", "The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.");
|
|
87
|
+
const outputs = state.outputs;
|
|
88
|
+
const driver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
89
|
+
const cache = /* @__PURE__ */ new Map();
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [key, output] of Object.entries(outputs)) {
|
|
92
|
+
if (!output?.ok || output.value == null) {
|
|
93
|
+
result[key] = { ok: output?.ok ?? false };
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
result[key] = await resolveHandlesInValue(output.value, driver, maxColumns, cache);
|
|
97
|
+
}
|
|
98
|
+
return textResult(result);
|
|
99
|
+
});
|
|
100
|
+
server.registerTool("list_columns", {
|
|
101
|
+
description: "List all columns in a PFrame with their specs. Use get_block_outputs first to find the PFrame handle.",
|
|
102
|
+
inputSchema: { pFrameHandle: z.string().describe("PFrame handle (64-char hex hash from get_block_outputs)") }
|
|
103
|
+
}, async ({ pFrameHandle }) => {
|
|
104
|
+
return textResult((await ctx.requireMl().internalDriverKit.pFrameDriver.listColumns(pFrameHandle)).map((c) => ({
|
|
105
|
+
columnId: c.columnId,
|
|
106
|
+
name: c.spec.name,
|
|
107
|
+
valueType: c.spec.valueType,
|
|
108
|
+
label: readAnnotation(c.spec, Annotation.Label),
|
|
109
|
+
visibility: readAnnotation(c.spec, Annotation.Table.Visibility),
|
|
110
|
+
axes: c.spec.axesSpec.map((a) => ({
|
|
111
|
+
name: a.name,
|
|
112
|
+
type: a.type,
|
|
113
|
+
label: readAnnotation(a, Annotation.Label)
|
|
114
|
+
}))
|
|
115
|
+
})));
|
|
116
|
+
});
|
|
117
|
+
server.registerTool("query_table", {
|
|
118
|
+
description: "Query data from a PTable. Returns rows as arrays of values. Use get_block_outputs first to find the PTable handle. Use `transform` to process results server-side and return only what you need.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
pTableHandle: z.string().describe("PTable handle (64-char hex hash from get_block_outputs)"),
|
|
121
|
+
columns: z.array(z.number()).optional().describe("Column indices to retrieve (default: all). Use get_block_outputs to see column indices."),
|
|
122
|
+
offset: z.number().optional().default(0).describe("Row offset (default 0)"),
|
|
123
|
+
limit: z.number().optional().default(50).describe("Number of rows to return (default 50)."),
|
|
124
|
+
maxLimit: z.number().optional().default(1e3).describe("Upper bound for limit (default 1000). Increase for large exports."),
|
|
125
|
+
transform: z.string().optional().describe("JS expression evaluated server-side against query results. Available variables: `rows` (array of row arrays), `columns` (column headers), `offset`, `rowCount`. Example: `rows.map(r => r[0])` — extract first column only."),
|
|
126
|
+
transformTimeout: z.number().optional().default(5e3).describe("Timeout in ms for transform evaluation (default 5000).")
|
|
127
|
+
}
|
|
128
|
+
}, async ({ pTableHandle, columns, offset, limit, maxLimit, transform, transformTimeout }) => {
|
|
129
|
+
const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;
|
|
130
|
+
const handle = pTableHandle;
|
|
131
|
+
let spec;
|
|
132
|
+
try {
|
|
133
|
+
spec = await pFrameDriver.getSpec(handle);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return textResult({ error: `getSpec failed: ${err}` });
|
|
136
|
+
}
|
|
137
|
+
const range = {
|
|
138
|
+
offset,
|
|
139
|
+
length: Math.min(limit, maxLimit)
|
|
140
|
+
};
|
|
141
|
+
const columnIndices = columns ?? spec.map((_, i) => i);
|
|
142
|
+
let vectors;
|
|
143
|
+
try {
|
|
144
|
+
vectors = await pFrameDriver.getData(handle, columnIndices, range);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return textResult({
|
|
147
|
+
error: `getData failed: ${err}`,
|
|
148
|
+
columnIndices,
|
|
149
|
+
range
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const actualRows = vectors.length > 0 ? vectors[0].data.length : 0;
|
|
153
|
+
const columnVectors = vectors.map((v) => vectorToJson(v, actualRows));
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (let r = 0; r < actualRows; r++) rows.push(columnVectors.map((col) => col[r]));
|
|
156
|
+
const columnHeaders = columnIndices.map((idx) => {
|
|
157
|
+
const s = spec[idx];
|
|
158
|
+
return {
|
|
159
|
+
index: idx,
|
|
160
|
+
type: s.type,
|
|
161
|
+
name: s.type === "column" ? s.spec.name : s.spec.name,
|
|
162
|
+
label: readAnnotation(s.spec, Annotation.Label)
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
if (transform) try {
|
|
166
|
+
return textResult(await safeEval(transform, {
|
|
167
|
+
rows,
|
|
168
|
+
columns: columnHeaders,
|
|
169
|
+
offset,
|
|
170
|
+
rowCount: actualRows
|
|
171
|
+
}, transformTimeout));
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return errorResult(`Transform failed: ${e instanceof Error ? e.message : String(e)}`, "Check your JS expression syntax. Available variables: rows, columns, offset, rowCount.");
|
|
174
|
+
}
|
|
175
|
+
return textResult({
|
|
176
|
+
offset,
|
|
177
|
+
rowCount: actualRows,
|
|
178
|
+
columns: columnHeaders,
|
|
179
|
+
rows
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { registerDataQueryTools };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=data-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-query.js","names":[],"sources":["../../src/tools/data-query.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type {\n PFrameHandle,\n PTableHandle,\n PTableColumnSpec,\n PTableVector,\n} from \"@milaboratories/pl-middle-layer\";\nimport {\n Annotation,\n pTableValue,\n readAnnotation,\n PFrameDriver,\n} from \"@milaboratories/pl-model-common\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { safeEval } from \"./sandbox\";\nimport { errorResult, textResult } from \"./types\";\n\nconst HEX_HASH_RE = /^[a-f0-9]{64}$/;\n\n/**\n * Try to resolve a 64-char hex handle as PTable, then PFrame.\n * Returns a summary object or the original string if neither works.\n */\nasync function resolveHandle(\n handle: string,\n driver: PFrameDriver,\n maxColumns: number,\n cache: Map<string, unknown>,\n): Promise<unknown> {\n if (cache.has(handle)) return cache.get(handle);\n\n // Try PTable first (has rows — more useful info)\n try {\n const spec = await driver.getSpec(handle as PTableHandle);\n const shape = await driver.getShape(handle as PTableHandle);\n const summary: Record<string, unknown> = {\n _type: \"PTable\",\n handle,\n rows: shape.rows,\n columnCount: spec.length,\n columns: spec.slice(0, maxColumns).map((s: PTableColumnSpec, idx: number) => ({\n index: idx,\n type: s.type,\n name: s.spec.name,\n valueType: s.type === \"column\" ? s.spec.valueType : s.spec.type,\n label: readAnnotation(s.spec, Annotation.Label),\n })),\n };\n if (spec.length > maxColumns) {\n summary.truncated = true;\n summary.showing = maxColumns;\n }\n cache.set(handle, summary);\n return summary;\n } catch {\n // not a PTable\n }\n\n // Try PFrame\n try {\n const columns = await driver.listColumns(handle as PFrameHandle);\n const summary: Record<string, unknown> = {\n _type: \"PFrame\",\n handle,\n columnCount: columns.length,\n columns: columns.slice(0, maxColumns).map((c) => ({\n name: c.spec.name,\n valueType: c.spec.valueType,\n label: readAnnotation(c.spec, Annotation.Label),\n })),\n };\n if (columns.length > maxColumns) {\n summary.truncated = true;\n summary.showing = maxColumns;\n }\n cache.set(handle, summary);\n return summary;\n } catch {\n // not a PFrame either\n }\n\n cache.set(handle, handle);\n return handle;\n}\n\n/**\n * Recursively walk a value tree, replacing 64-char hex handles\n * with PFrame/PTable summaries (column specs + row count).\n */\nasync function resolveHandlesInValue(\n value: unknown,\n driver: Parameters<typeof resolveHandle>[1],\n maxColumns: number,\n cache: Map<string, unknown>,\n): Promise<unknown> {\n if (value === null || value === undefined) return value;\n if (typeof value === \"string\") {\n return HEX_HASH_RE.test(value) ? resolveHandle(value, driver, maxColumns, cache) : value;\n }\n if (typeof value !== \"object\") return value;\n if (Array.isArray(value)) {\n return Promise.all(value.map((v) => resolveHandlesInValue(v, driver, maxColumns, cache)));\n }\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = await resolveHandlesInValue(v, driver, maxColumns, cache);\n }\n return out;\n}\n\n/** Converts a PTableVector column to a JSON-serializable array using the SDK helper. */\nfunction vectorToJson(vector: PTableVector, rows: number): (string | number | null)[] {\n const result: (string | number | null)[] = [];\n for (let i = 0; i < rows; i++) {\n result.push(pTableValue(vector, i));\n }\n return result;\n}\n\nexport function registerDataQueryTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_block_outputs\",\n {\n description:\n \"Get block output values as a JSON map. \" +\n \"PFrame/PTable handles are resolved inline to summaries with column specs and row counts. \" +\n \"Use this to discover block results and available data before querying tables.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n maxColumns: z\n .number()\n .optional()\n .default(30)\n .describe(\"Max columns to show per PFrame/PTable summary (default 30).\"),\n },\n },\n async ({ projectId, blockId, maxColumns }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n if (!state.outputs)\n return errorResult(\n \"Block has no outputs yet.\",\n \"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.\",\n );\n\n const outputs = state.outputs as Record<string, { ok?: boolean; value?: unknown }>;\n const driver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const cache = new Map<string, unknown>();\n\n const result: Record<string, unknown> = {};\n for (const [key, output] of Object.entries(outputs)) {\n if (!output?.ok || output.value == null) {\n result[key] = { ok: output?.ok ?? false };\n continue;\n }\n result[key] = await resolveHandlesInValue(output.value, driver, maxColumns, cache);\n }\n\n return textResult(result);\n },\n );\n\n server.registerTool(\n \"list_columns\",\n {\n description:\n \"List all columns in a PFrame with their specs. Use get_block_outputs first to find the PFrame handle.\",\n inputSchema: {\n pFrameHandle: z\n .string()\n .describe(\"PFrame handle (64-char hex hash from get_block_outputs)\"),\n },\n },\n async ({ pFrameHandle }) => {\n const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const columns = await pFrameDriver.listColumns(pFrameHandle as PFrameHandle);\n return textResult(\n columns.map((c) => ({\n columnId: c.columnId,\n name: c.spec.name,\n valueType: c.spec.valueType,\n label: readAnnotation(c.spec, Annotation.Label),\n visibility: readAnnotation(c.spec, Annotation.Table.Visibility),\n axes: c.spec.axesSpec.map((a) => ({\n name: a.name,\n type: a.type,\n label: readAnnotation(a, Annotation.Label),\n })),\n })),\n );\n },\n );\n\n server.registerTool(\n \"query_table\",\n {\n description:\n \"Query data from a PTable. Returns rows as arrays of values. Use get_block_outputs first to find the PTable handle. \" +\n \"Use `transform` to process results server-side and return only what you need.\",\n inputSchema: {\n pTableHandle: z\n .string()\n .describe(\"PTable handle (64-char hex hash from get_block_outputs)\"),\n columns: z\n .array(z.number())\n .optional()\n .describe(\n \"Column indices to retrieve (default: all). Use get_block_outputs to see column indices.\",\n ),\n offset: z.number().optional().default(0).describe(\"Row offset (default 0)\"),\n limit: z.number().optional().default(50).describe(\"Number of rows to return (default 50).\"),\n maxLimit: z\n .number()\n .optional()\n .default(1000)\n .describe(\"Upper bound for limit (default 1000). Increase for large exports.\"),\n transform: z\n .string()\n .optional()\n .describe(\n \"JS expression evaluated server-side against query results. \" +\n \"Available variables: `rows` (array of row arrays), `columns` (column headers), `offset`, `rowCount`. \" +\n \"Example: `rows.map(r => r[0])` — extract first column only.\",\n ),\n transformTimeout: z\n .number()\n .optional()\n .default(5000)\n .describe(\"Timeout in ms for transform evaluation (default 5000).\"),\n },\n },\n async ({ pTableHandle, columns, offset, limit, maxLimit, transform, transformTimeout }) => {\n const pFrameDriver = ctx.requireMl().internalDriverKit.pFrameDriver;\n const handle = pTableHandle as PTableHandle;\n\n let spec;\n try {\n spec = await pFrameDriver.getSpec(handle);\n } catch (err) {\n return textResult({ error: `getSpec failed: ${err}` });\n }\n\n const effectiveLimit = Math.min(limit, maxLimit);\n const range = { offset, length: effectiveLimit };\n\n // If no columns specified, get all\n const columnIndices = columns ?? spec.map((_: PTableColumnSpec, i: number) => i);\n\n let vectors: PTableVector[];\n try {\n vectors = await pFrameDriver.getData(handle, columnIndices, range);\n } catch (err) {\n return textResult({\n error: `getData failed: ${err}`,\n columnIndices,\n range,\n });\n }\n\n const actualRows = vectors.length > 0 ? vectors[0].data.length : 0;\n const columnVectors = vectors.map((v) => vectorToJson(v, actualRows));\n const rows: unknown[][] = [];\n for (let r = 0; r < actualRows; r++) {\n rows.push(columnVectors.map((col) => col[r]));\n }\n\n const columnHeaders = columnIndices.map((idx: number) => {\n const s = spec[idx];\n return {\n index: idx,\n type: s.type,\n name: s.type === \"column\" ? s.spec.name : s.spec.name,\n label: readAnnotation(s.spec, Annotation.Label),\n };\n });\n\n if (transform) {\n try {\n const result = await safeEval(\n transform,\n {\n rows,\n columns: columnHeaders,\n offset,\n rowCount: actualRows,\n },\n transformTimeout,\n );\n return textResult(result);\n } catch (e: unknown) {\n return errorResult(\n `Transform failed: ${e instanceof Error ? e.message : String(e)}`,\n \"Check your JS expression syntax. Available variables: rows, columns, offset, rowCount.\",\n );\n }\n }\n\n return textResult({\n offset,\n rowCount: actualRows,\n columns: columnHeaders,\n rows,\n });\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const require_types = require("./types.cjs");
|
|
2
|
+
let zod = require("zod");
|
|
3
|
+
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
4
|
+
//#region src/tools/logs.ts
|
|
5
|
+
function registerLogTools(server, ctx) {
|
|
6
|
+
server.registerTool("get_block_logs", {
|
|
7
|
+
description: "Read execution logs for a block. Extracts log handles from block outputs and reads log content. Returns logs keyed by sample/run ID.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
projectId: zod.z.string().describe("Project ID"),
|
|
10
|
+
blockId: zod.z.string().describe("Block ID"),
|
|
11
|
+
lines: zod.z.number().optional().default(100).describe("Number of lines per log (default 100)"),
|
|
12
|
+
sampleId: zod.z.string().optional().describe("Specific sample/key to read logs for (reads all if omitted)")
|
|
13
|
+
}
|
|
14
|
+
}, async ({ projectId, blockId, lines, sampleId }) => {
|
|
15
|
+
const state = await (await ctx.getOpenedProject(projectId)).getBlockState(blockId).getValue();
|
|
16
|
+
if (!state.outputs) return require_types.errorResult("Block has no outputs yet.", "The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.");
|
|
17
|
+
const outputs = state.outputs;
|
|
18
|
+
const logEntries = [];
|
|
19
|
+
for (const [outputKey, output] of Object.entries(outputs)) {
|
|
20
|
+
if (!output?.ok || !output.value?.data) continue;
|
|
21
|
+
for (const entry of output.value.data) if ((0, _milaboratories_pl_model_common.isAnyLogHandle)(entry.value)) logEntries.push({
|
|
22
|
+
outputKey,
|
|
23
|
+
key: entry.key,
|
|
24
|
+
handle: entry.value
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (logEntries.length === 0) return require_types.errorResult("No log handles found in block outputs.", "This block may not produce logs, or it hasn't run yet. Use get_block_outputs to inspect available output types.");
|
|
28
|
+
const logDriver = ctx.requireMl().driverKit.logDriver;
|
|
29
|
+
const results = {};
|
|
30
|
+
for (const entry of logEntries) {
|
|
31
|
+
const key = entry.key.join("/");
|
|
32
|
+
if (sampleId && !entry.key.includes(sampleId)) continue;
|
|
33
|
+
try {
|
|
34
|
+
const response = await logDriver.lastLines(entry.handle, lines);
|
|
35
|
+
if (response.shouldUpdateHandle) results[key] = "[log handle stale — block may still be running, retry later]";
|
|
36
|
+
else results[key] = new TextDecoder().decode(response.data);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
results[key] = `Error reading log: ${err}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return require_types.textResult(results);
|
|
42
|
+
});
|
|
43
|
+
server.registerTool("get_app_log", {
|
|
44
|
+
description: "Read recent lines from the application log. Useful for debugging errors.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
lines: zod.z.number().optional().default(50).describe("Number of lines to return (default 50)"),
|
|
47
|
+
search: zod.z.string().optional().describe("Filter lines containing this substring")
|
|
48
|
+
}
|
|
49
|
+
}, async ({ lines, search }) => {
|
|
50
|
+
if (!ctx.callbacks.readAppLog) return require_types.errorResult("App log reading is not available.", "This feature requires the desktop app integration.");
|
|
51
|
+
return require_types.textResult({ log: await ctx.callbacks.readAppLog(lines, search) });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
exports.registerLogTools = registerLogTools;
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=logs.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.cjs","names":["z","errorResult","textResult"],"sources":["../../src/tools/logs.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isAnyLogHandle } from \"@milaboratories/pl-model-common\";\nimport type { AnyLogHandle } from \"@milaboratories/pl-model-common\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerLogTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_block_logs\",\n {\n description:\n \"Read execution logs for a block. Extracts log handles from block outputs and reads log content. Returns logs keyed by sample/run ID.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n lines: z.number().optional().default(100).describe(\"Number of lines per log (default 100)\"),\n sampleId: z\n .string()\n .optional()\n .describe(\"Specific sample/key to read logs for (reads all if omitted)\"),\n },\n },\n async ({ projectId, blockId, lines, sampleId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n if (!state.outputs)\n return errorResult(\n \"Block has no outputs yet.\",\n \"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.\",\n );\n\n // Scan all outputs for log handles (log+ready:// or log+live://)\n const outputs = state.outputs as Record<\n string,\n { ok?: boolean; value?: { data?: { key: string[]; value: unknown }[] } }\n >;\n const logEntries: { outputKey: string; key: string[]; handle: AnyLogHandle }[] = [];\n for (const [outputKey, output] of Object.entries(outputs)) {\n if (!output?.ok || !output.value?.data) continue;\n for (const entry of output.value.data) {\n if (isAnyLogHandle(entry.value)) {\n logEntries.push({ outputKey, key: entry.key, handle: entry.value });\n }\n }\n }\n\n if (logEntries.length === 0) {\n return errorResult(\n \"No log handles found in block outputs.\",\n \"This block may not produce logs, or it hasn't run yet. Use get_block_outputs to inspect available output types.\",\n );\n }\n\n const logDriver = ctx.requireMl().driverKit.logDriver;\n const results: Record<string, string> = {};\n\n for (const entry of logEntries) {\n const key = entry.key.join(\"/\");\n if (sampleId && !entry.key.includes(sampleId)) continue;\n try {\n const response = await logDriver.lastLines(entry.handle, lines);\n if (response.shouldUpdateHandle) {\n results[key] = \"[log handle stale — block may still be running, retry later]\";\n } else {\n results[key] = new TextDecoder().decode(response.data);\n }\n } catch (err) {\n results[key] = `Error reading log: ${err}`;\n }\n }\n\n return textResult(results);\n },\n );\n\n server.registerTool(\n \"get_app_log\",\n {\n description: \"Read recent lines from the application log. Useful for debugging errors.\",\n inputSchema: {\n lines: z.number().optional().default(50).describe(\"Number of lines to return (default 50)\"),\n search: z.string().optional().describe(\"Filter lines containing this substring\"),\n },\n },\n async ({ lines, search }) => {\n if (!ctx.callbacks.readAppLog) {\n return errorResult(\n \"App log reading is not available.\",\n \"This feature requires the desktop app integration.\",\n );\n }\n const log = await ctx.callbacks.readAppLog(lines, search);\n return textResult({ log });\n },\n );\n}\n"],"mappings":";;;;AAOA,SAAgB,iBAAiB,QAAmB,KAAwB;AAC1E,QAAO,aACL,kBACA;EACE,aACE;EACF,aAAa;GACX,WAAWA,IAAAA,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAASA,IAAAA,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,OAAOA,IAAAA,EAAE,QAAQ,CAAC,UAAU,CAAC,QAAQ,IAAI,CAAC,SAAS,wCAAwC;GAC3F,UAAUA,IAAAA,EACP,QAAQ,CACR,UAAU,CACV,SAAS,8DAA8D;GAC3E;EACF,EACD,OAAO,EAAE,WAAW,SAAS,OAAO,eAAe;EAEjD,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;AAC7D,MAAI,CAAC,MAAM,QACT,QAAOC,cAAAA,YACL,6BACA,sHACD;EAGH,MAAM,UAAU,MAAM;EAItB,MAAM,aAA2E,EAAE;AACnF,OAAK,MAAM,CAAC,WAAW,WAAW,OAAO,QAAQ,QAAQ,EAAE;AACzD,OAAI,CAAC,QAAQ,MAAM,CAAC,OAAO,OAAO,KAAM;AACxC,QAAK,MAAM,SAAS,OAAO,MAAM,KAC/B,MAAA,GAAA,gCAAA,gBAAmB,MAAM,MAAM,CAC7B,YAAW,KAAK;IAAE;IAAW,KAAK,MAAM;IAAK,QAAQ,MAAM;IAAO,CAAC;;AAKzE,MAAI,WAAW,WAAW,EACxB,QAAOA,cAAAA,YACL,0CACA,kHACD;EAGH,MAAM,YAAY,IAAI,WAAW,CAAC,UAAU;EAC5C,MAAM,UAAkC,EAAE;AAE1C,OAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,MAAM,MAAM,IAAI,KAAK,IAAI;AAC/B,OAAI,YAAY,CAAC,MAAM,IAAI,SAAS,SAAS,CAAE;AAC/C,OAAI;IACF,MAAM,WAAW,MAAM,UAAU,UAAU,MAAM,QAAQ,MAAM;AAC/D,QAAI,SAAS,mBACX,SAAQ,OAAO;QAEf,SAAQ,OAAO,IAAI,aAAa,CAAC,OAAO,SAAS,KAAK;YAEjD,KAAK;AACZ,YAAQ,OAAO,sBAAsB;;;AAIzC,SAAOC,cAAAA,WAAW,QAAQ;GAE7B;AAED,QAAO,aACL,eACA;EACE,aAAa;EACb,aAAa;GACX,OAAOF,IAAAA,EAAE,QAAQ,CAAC,UAAU,CAAC,QAAQ,GAAG,CAAC,SAAS,yCAAyC;GAC3F,QAAQA,IAAAA,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,yCAAyC;GACjF;EACF,EACD,OAAO,EAAE,OAAO,aAAa;AAC3B,MAAI,CAAC,IAAI,UAAU,WACjB,QAAOC,cAAAA,YACL,qCACA,qDACD;AAGH,SAAOC,cAAAA,WAAW,EAAE,KADR,MAAM,IAAI,UAAU,WAAW,OAAO,OAAO,EAChC,CAAC;GAE7B"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { errorResult, textResult } from "./types.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { isAnyLogHandle } from "@milaboratories/pl-model-common";
|
|
4
|
+
//#region src/tools/logs.ts
|
|
5
|
+
function registerLogTools(server, ctx) {
|
|
6
|
+
server.registerTool("get_block_logs", {
|
|
7
|
+
description: "Read execution logs for a block. Extracts log handles from block outputs and reads log content. Returns logs keyed by sample/run ID.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
projectId: z.string().describe("Project ID"),
|
|
10
|
+
blockId: z.string().describe("Block ID"),
|
|
11
|
+
lines: z.number().optional().default(100).describe("Number of lines per log (default 100)"),
|
|
12
|
+
sampleId: z.string().optional().describe("Specific sample/key to read logs for (reads all if omitted)")
|
|
13
|
+
}
|
|
14
|
+
}, async ({ projectId, blockId, lines, sampleId }) => {
|
|
15
|
+
const state = await (await ctx.getOpenedProject(projectId)).getBlockState(blockId).getValue();
|
|
16
|
+
if (!state.outputs) return errorResult("Block has no outputs yet.", "The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.");
|
|
17
|
+
const outputs = state.outputs;
|
|
18
|
+
const logEntries = [];
|
|
19
|
+
for (const [outputKey, output] of Object.entries(outputs)) {
|
|
20
|
+
if (!output?.ok || !output.value?.data) continue;
|
|
21
|
+
for (const entry of output.value.data) if (isAnyLogHandle(entry.value)) logEntries.push({
|
|
22
|
+
outputKey,
|
|
23
|
+
key: entry.key,
|
|
24
|
+
handle: entry.value
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (logEntries.length === 0) return errorResult("No log handles found in block outputs.", "This block may not produce logs, or it hasn't run yet. Use get_block_outputs to inspect available output types.");
|
|
28
|
+
const logDriver = ctx.requireMl().driverKit.logDriver;
|
|
29
|
+
const results = {};
|
|
30
|
+
for (const entry of logEntries) {
|
|
31
|
+
const key = entry.key.join("/");
|
|
32
|
+
if (sampleId && !entry.key.includes(sampleId)) continue;
|
|
33
|
+
try {
|
|
34
|
+
const response = await logDriver.lastLines(entry.handle, lines);
|
|
35
|
+
if (response.shouldUpdateHandle) results[key] = "[log handle stale — block may still be running, retry later]";
|
|
36
|
+
else results[key] = new TextDecoder().decode(response.data);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
results[key] = `Error reading log: ${err}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return textResult(results);
|
|
42
|
+
});
|
|
43
|
+
server.registerTool("get_app_log", {
|
|
44
|
+
description: "Read recent lines from the application log. Useful for debugging errors.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
lines: z.number().optional().default(50).describe("Number of lines to return (default 50)"),
|
|
47
|
+
search: z.string().optional().describe("Filter lines containing this substring")
|
|
48
|
+
}
|
|
49
|
+
}, async ({ lines, search }) => {
|
|
50
|
+
if (!ctx.callbacks.readAppLog) return errorResult("App log reading is not available.", "This feature requires the desktop app integration.");
|
|
51
|
+
return textResult({ log: await ctx.callbacks.readAppLog(lines, search) });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { registerLogTools };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.js","names":[],"sources":["../../src/tools/logs.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isAnyLogHandle } from \"@milaboratories/pl-model-common\";\nimport type { AnyLogHandle } from \"@milaboratories/pl-model-common\";\nimport { z } from \"zod\";\nimport type { ToolContext } from \"./types\";\nimport { errorResult, textResult } from \"./types\";\n\nexport function registerLogTools(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n \"get_block_logs\",\n {\n description:\n \"Read execution logs for a block. Extracts log handles from block outputs and reads log content. Returns logs keyed by sample/run ID.\",\n inputSchema: {\n projectId: z.string().describe(\"Project ID\"),\n blockId: z.string().describe(\"Block ID\"),\n lines: z.number().optional().default(100).describe(\"Number of lines per log (default 100)\"),\n sampleId: z\n .string()\n .optional()\n .describe(\"Specific sample/key to read logs for (reads all if omitted)\"),\n },\n },\n async ({ projectId, blockId, lines, sampleId }) => {\n const project = await ctx.getOpenedProject(projectId);\n const state = await project.getBlockState(blockId).getValue();\n if (!state.outputs)\n return errorResult(\n \"Block has no outputs yet.\",\n \"The block may not have been run. Use get_project_overview to check its calculationStatus, then run_block if needed.\",\n );\n\n // Scan all outputs for log handles (log+ready:// or log+live://)\n const outputs = state.outputs as Record<\n string,\n { ok?: boolean; value?: { data?: { key: string[]; value: unknown }[] } }\n >;\n const logEntries: { outputKey: string; key: string[]; handle: AnyLogHandle }[] = [];\n for (const [outputKey, output] of Object.entries(outputs)) {\n if (!output?.ok || !output.value?.data) continue;\n for (const entry of output.value.data) {\n if (isAnyLogHandle(entry.value)) {\n logEntries.push({ outputKey, key: entry.key, handle: entry.value });\n }\n }\n }\n\n if (logEntries.length === 0) {\n return errorResult(\n \"No log handles found in block outputs.\",\n \"This block may not produce logs, or it hasn't run yet. Use get_block_outputs to inspect available output types.\",\n );\n }\n\n const logDriver = ctx.requireMl().driverKit.logDriver;\n const results: Record<string, string> = {};\n\n for (const entry of logEntries) {\n const key = entry.key.join(\"/\");\n if (sampleId && !entry.key.includes(sampleId)) continue;\n try {\n const response = await logDriver.lastLines(entry.handle, lines);\n if (response.shouldUpdateHandle) {\n results[key] = \"[log handle stale — block may still be running, retry later]\";\n } else {\n results[key] = new TextDecoder().decode(response.data);\n }\n } catch (err) {\n results[key] = `Error reading log: ${err}`;\n }\n }\n\n return textResult(results);\n },\n );\n\n server.registerTool(\n \"get_app_log\",\n {\n description: \"Read recent lines from the application log. Useful for debugging errors.\",\n inputSchema: {\n lines: z.number().optional().default(50).describe(\"Number of lines to return (default 50)\"),\n search: z.string().optional().describe(\"Filter lines containing this substring\"),\n },\n },\n async ({ lines, search }) => {\n if (!ctx.callbacks.readAppLog) {\n return errorResult(\n \"App log reading is not available.\",\n \"This feature requires the desktop app integration.\",\n );\n }\n const log = await ctx.callbacks.readAppLog(lines, search);\n return textResult({ log });\n },\n );\n}\n"],"mappings":";;;;AAOA,SAAgB,iBAAiB,QAAmB,KAAwB;AAC1E,QAAO,aACL,kBACA;EACE,aACE;EACF,aAAa;GACX,WAAW,EAAE,QAAQ,CAAC,SAAS,aAAa;GAC5C,SAAS,EAAE,QAAQ,CAAC,SAAS,WAAW;GACxC,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,QAAQ,IAAI,CAAC,SAAS,wCAAwC;GAC3F,UAAU,EACP,QAAQ,CACR,UAAU,CACV,SAAS,8DAA8D;GAC3E;EACF,EACD,OAAO,EAAE,WAAW,SAAS,OAAO,eAAe;EAEjD,MAAM,QAAQ,OADE,MAAM,IAAI,iBAAiB,UAAU,EACzB,cAAc,QAAQ,CAAC,UAAU;AAC7D,MAAI,CAAC,MAAM,QACT,QAAO,YACL,6BACA,sHACD;EAGH,MAAM,UAAU,MAAM;EAItB,MAAM,aAA2E,EAAE;AACnF,OAAK,MAAM,CAAC,WAAW,WAAW,OAAO,QAAQ,QAAQ,EAAE;AACzD,OAAI,CAAC,QAAQ,MAAM,CAAC,OAAO,OAAO,KAAM;AACxC,QAAK,MAAM,SAAS,OAAO,MAAM,KAC/B,KAAI,eAAe,MAAM,MAAM,CAC7B,YAAW,KAAK;IAAE;IAAW,KAAK,MAAM;IAAK,QAAQ,MAAM;IAAO,CAAC;;AAKzE,MAAI,WAAW,WAAW,EACxB,QAAO,YACL,0CACA,kHACD;EAGH,MAAM,YAAY,IAAI,WAAW,CAAC,UAAU;EAC5C,MAAM,UAAkC,EAAE;AAE1C,OAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,MAAM,MAAM,IAAI,KAAK,IAAI;AAC/B,OAAI,YAAY,CAAC,MAAM,IAAI,SAAS,SAAS,CAAE;AAC/C,OAAI;IACF,MAAM,WAAW,MAAM,UAAU,UAAU,MAAM,QAAQ,MAAM;AAC/D,QAAI,SAAS,mBACX,SAAQ,OAAO;QAEf,SAAQ,OAAO,IAAI,aAAa,CAAC,OAAO,SAAS,KAAK;YAEjD,KAAK;AACZ,YAAQ,OAAO,sBAAsB;;;AAIzC,SAAO,WAAW,QAAQ;GAE7B;AAED,QAAO,aACL,eACA;EACE,aAAa;EACb,aAAa;GACX,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,QAAQ,GAAG,CAAC,SAAS,yCAAyC;GAC3F,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,yCAAyC;GACjF;EACF,EACD,OAAO,EAAE,OAAO,aAAa;AAC3B,MAAI,CAAC,IAAI,UAAU,WACjB,QAAO,YACL,qCACA,qDACD;AAGH,SAAO,WAAW,EAAE,KADR,MAAM,IAAI,UAAU,WAAW,OAAO,OAAO,EAChC,CAAC;GAE7B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const require_types = require("./types.cjs");
|
|
2
|
+
//#region src/tools/ping.ts
|
|
3
|
+
function registerPingTool(server, ctx) {
|
|
4
|
+
server.registerTool("ping", { description: "Health check" }, async () => {
|
|
5
|
+
return require_types.textResult({
|
|
6
|
+
status: "ok",
|
|
7
|
+
connected: !!ctx.getMl()
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
exports.registerPingTool = registerPingTool;
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=ping.cjs.map
|