@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.
Files changed (78) hide show
  1. package/dist/index.cjs +3 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/server.cjs +171 -0
  5. package/dist/server.cjs.map +1 -0
  6. package/dist/server.d.ts +83 -0
  7. package/dist/server.d.ts.map +1 -0
  8. package/dist/server.js +171 -0
  9. package/dist/server.js.map +1 -0
  10. package/dist/tools/await.cjs +89 -0
  11. package/dist/tools/await.cjs.map +1 -0
  12. package/dist/tools/await.js +89 -0
  13. package/dist/tools/await.js.map +1 -0
  14. package/dist/tools/block-state.cjs +71 -0
  15. package/dist/tools/block-state.cjs.map +1 -0
  16. package/dist/tools/block-state.js +71 -0
  17. package/dist/tools/block-state.js.map +1 -0
  18. package/dist/tools/blocks.cjs +123 -0
  19. package/dist/tools/blocks.cjs.map +1 -0
  20. package/dist/tools/blocks.js +123 -0
  21. package/dist/tools/blocks.js.map +1 -0
  22. package/dist/tools/connection.cjs +33 -0
  23. package/dist/tools/connection.cjs.map +1 -0
  24. package/dist/tools/connection.js +33 -0
  25. package/dist/tools/connection.js.map +1 -0
  26. package/dist/tools/data-query.cjs +186 -0
  27. package/dist/tools/data-query.cjs.map +1 -0
  28. package/dist/tools/data-query.js +186 -0
  29. package/dist/tools/data-query.js.map +1 -0
  30. package/dist/tools/logs.cjs +57 -0
  31. package/dist/tools/logs.cjs.map +1 -0
  32. package/dist/tools/logs.js +57 -0
  33. package/dist/tools/logs.js.map +1 -0
  34. package/dist/tools/ping.cjs +14 -0
  35. package/dist/tools/ping.cjs.map +1 -0
  36. package/dist/tools/ping.js +14 -0
  37. package/dist/tools/ping.js.map +1 -0
  38. package/dist/tools/projects.cjs +56 -0
  39. package/dist/tools/projects.cjs.map +1 -0
  40. package/dist/tools/projects.js +56 -0
  41. package/dist/tools/projects.js.map +1 -0
  42. package/dist/tools/sandbox.cjs +51 -0
  43. package/dist/tools/sandbox.cjs.map +1 -0
  44. package/dist/tools/sandbox.js +51 -0
  45. package/dist/tools/sandbox.js.map +1 -0
  46. package/dist/tools/screenshot.cjs +35 -0
  47. package/dist/tools/screenshot.cjs.map +1 -0
  48. package/dist/tools/screenshot.js +35 -0
  49. package/dist/tools/screenshot.js.map +1 -0
  50. package/dist/tools/tokens.cjs +82 -0
  51. package/dist/tools/tokens.cjs.map +1 -0
  52. package/dist/tools/tokens.js +82 -0
  53. package/dist/tools/tokens.js.map +1 -0
  54. package/dist/tools/types.cjs +22 -0
  55. package/dist/tools/types.cjs.map +1 -0
  56. package/dist/tools/types.js +21 -0
  57. package/dist/tools/types.js.map +1 -0
  58. package/dist/tools/ui-interaction.cjs +117 -0
  59. package/dist/tools/ui-interaction.cjs.map +1 -0
  60. package/dist/tools/ui-interaction.js +117 -0
  61. package/dist/tools/ui-interaction.js.map +1 -0
  62. package/package.json +56 -0
  63. package/src/index.ts +7 -0
  64. package/src/server.ts +271 -0
  65. package/src/tools/await.ts +151 -0
  66. package/src/tools/block-state.ts +115 -0
  67. package/src/tools/blocks.ts +222 -0
  68. package/src/tools/connection.ts +63 -0
  69. package/src/tools/data-query.ts +308 -0
  70. package/src/tools/logs.ts +97 -0
  71. package/src/tools/ping.ts +9 -0
  72. package/src/tools/projects.ts +84 -0
  73. package/src/tools/sandbox.ts +62 -0
  74. package/src/tools/screenshot.ts +48 -0
  75. package/src/tools/tokens.test.ts +239 -0
  76. package/src/tools/tokens.ts +84 -0
  77. package/src/tools/types.ts +34 -0
  78. package/src/tools/ui-interaction.ts +156 -0
@@ -0,0 +1,151 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { isTimeoutError } from "@milaboratories/pl-middle-layer";
3
+ import { deriveDataFromStorage } from "@platforma-sdk/model";
4
+ import { z } from "zod";
5
+ import type { ToolContext } from "./types";
6
+ import { summarizeOutputs } from "./tokens";
7
+ import { safeEval } from "./sandbox";
8
+ import { errorResult, textResult } from "./types";
9
+
10
+ export function registerAwaitTools(server: McpServer, ctx: ToolContext): void {
11
+ server.registerTool(
12
+ "await_block_done",
13
+ {
14
+ description:
15
+ "Wait for a block to finish computation and outputs to stabilize. " +
16
+ "Returns block status, data, and concise output summary with token estimates. " +
17
+ "Use `transform` to extract specific data server-side on completion.",
18
+ inputSchema: {
19
+ projectId: z.string().describe("Project ID"),
20
+ blockId: z.string().describe("Block ID to wait for"),
21
+ timeout: z.number().optional().default(120000).describe("Timeout in ms (default 120000)"),
22
+ transform: z
23
+ .string()
24
+ .optional()
25
+ .describe(
26
+ "JS expression evaluated server-side when block completes. " +
27
+ "Available variables: `data` (block args), `outputs` (raw outputs), `block` (status info). " +
28
+ "Omit for default concise summary.",
29
+ ),
30
+ transformTimeout: z
31
+ .number()
32
+ .optional()
33
+ .default(5000)
34
+ .describe("Timeout in ms for transform evaluation (default 5000)."),
35
+ },
36
+ },
37
+ async ({ projectId, blockId, timeout, transform, transformTimeout }) => {
38
+ const project = await ctx.getOpenedProject(projectId);
39
+ const deadline = Date.now() + timeout;
40
+
41
+ while (Date.now() < deadline) {
42
+ const overview = await project.overview.getValue();
43
+ if (!overview) continue;
44
+ const block = overview.blocks.find((b) => b.id === blockId);
45
+ if (!block)
46
+ return errorResult(
47
+ `Block ${blockId} not found in project ${projectId}.`,
48
+ "Use get_project_overview to list all block IDs in this project.",
49
+ );
50
+
51
+ // Terminal error states — return immediately
52
+ if (block.calculationStatus === "Limbo") {
53
+ return errorResult(
54
+ "Block entered Limbo state (upstream failed or was stopped).",
55
+ "Check upstream blocks with get_project_overview. Fix or re-run the failed upstream, then retry.",
56
+ );
57
+ }
58
+
59
+ if (block.calculationStatus === "NotCalculated") {
60
+ return errorResult(
61
+ "Block has not been started.",
62
+ "Use run_block to start it first, then call await_block_done.",
63
+ );
64
+ }
65
+
66
+ if (block.calculationStatus === "Done") {
67
+ // Await stable block state with remaining time budget
68
+ const remaining = Math.max(deadline - Date.now(), 1000);
69
+ let state;
70
+ try {
71
+ state = await project
72
+ .getBlockState(blockId)
73
+ .awaitStableValue(AbortSignal.timeout(remaining));
74
+ } catch (e: unknown) {
75
+ if (isTimeoutError(e)) {
76
+ return textResult({
77
+ timedOut: true,
78
+ status: "Done",
79
+ note: "Computation done but outputs did not stabilize in time. Retry with a longer timeout.",
80
+ });
81
+ }
82
+ return errorResult(
83
+ `Failed to get block state: ${e instanceof Error ? e.message : String(e)}`,
84
+ );
85
+ }
86
+
87
+ const data = deriveDataFromStorage(state.blockStorage);
88
+
89
+ const blockInfo = {
90
+ id: block.id,
91
+ title: block.title ?? block.label,
92
+ calculationStatus: block.calculationStatus,
93
+ canRun: block.canRun,
94
+ stale: block.stale,
95
+ outputErrors: block.outputErrors,
96
+ };
97
+
98
+ if (transform) {
99
+ try {
100
+ const result = await safeEval(
101
+ transform,
102
+ {
103
+ data,
104
+ outputs: state.outputs,
105
+ block: blockInfo,
106
+ },
107
+ transformTimeout,
108
+ );
109
+ return textResult({ status: "Done", block: blockInfo, result });
110
+ } catch (e: unknown) {
111
+ return errorResult(
112
+ `Transform failed: ${e instanceof Error ? e.message : String(e)}`,
113
+ "Check your JS expression syntax. Available variables: data, outputs, block.",
114
+ );
115
+ }
116
+ }
117
+
118
+ return textResult({
119
+ status: "Done",
120
+ block: blockInfo,
121
+ data,
122
+ outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),
123
+ });
124
+ }
125
+
126
+ // Still running — wait up to 5s for overview to change, then re-poll.
127
+ // Minimum 500ms delay to avoid busy-looping if awaitChange resolves immediately.
128
+ const pollStart = Date.now();
129
+ try {
130
+ const result = await project.overview.getFullValue();
131
+ await project.overview.awaitChange(AbortSignal.timeout(5000), result.uTag);
132
+ } catch {
133
+ // timeout or abort — just re-poll
134
+ }
135
+ const elapsed = Date.now() - pollStart;
136
+ if (elapsed < 500) {
137
+ await new Promise((r) => setTimeout(r, 500 - elapsed));
138
+ }
139
+ }
140
+
141
+ // Timed out while running
142
+ const overview = await project.overview.getValue();
143
+ const block = overview?.blocks.find((b) => b.id === blockId);
144
+ return textResult({
145
+ timedOut: true,
146
+ status: block?.calculationStatus ?? "Unknown",
147
+ hint: "The block is still running. Call await_block_done again with a longer timeout.",
148
+ });
149
+ },
150
+ );
151
+ }
@@ -0,0 +1,115 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { deriveDataFromStorage } from "@platforma-sdk/model";
3
+ import { z } from "zod";
4
+ import type { ToolContext } from "./types";
5
+ import { summarizeOutputs } from "./tokens";
6
+ import { safeEval } from "./sandbox";
7
+ import { errorResult, textResult } from "./types";
8
+
9
+ export function registerBlockStateTools(server: McpServer, ctx: ToolContext): void {
10
+ server.registerTool(
11
+ "get_project_overview",
12
+ {
13
+ description:
14
+ "Get project overview with all blocks and their statuses (calculationStatus, canRun, stale, errors, upstreams/downstreams)",
15
+ inputSchema: {
16
+ projectId: z.string().describe("Project ID (must be opened)"),
17
+ },
18
+ },
19
+ async ({ projectId }) => {
20
+ const project = await ctx.getOpenedProject(projectId);
21
+ const overview = await project.overview.getValue();
22
+ if (!overview) return errorResult("Project overview not available yet.");
23
+ return textResult({
24
+ label: overview.meta.label,
25
+ blocks: overview.blocks.map((b) => ({
26
+ id: b.id,
27
+ title: b.title ?? b.label,
28
+ calculationStatus: b.calculationStatus,
29
+ canRun: b.canRun,
30
+ stale: b.stale,
31
+ inputsValid: b.inputsValid,
32
+ outputErrors: b.outputErrors,
33
+ upstreams: b.upstreams,
34
+ downstreams: b.downstreams,
35
+ })),
36
+ });
37
+ },
38
+ );
39
+
40
+ server.registerTool(
41
+ "get_block_state",
42
+ {
43
+ description:
44
+ "Get block state. Returns block args (data) and a concise output summary with token estimates by default. " +
45
+ "Use `transform` to extract specific data server-side without loading full outputs into context.\n\n" +
46
+ "Default: returns `{ data, outputs: [{ key, ok, hasValue, tokensEstimate }] }`\n\n" +
47
+ "Transform examples:\n" +
48
+ "- `outputs.logs?.value` — get one specific output value\n" +
49
+ "- `data` — get only block args\n" +
50
+ "- `({ preset: outputs.preset?.value, qc: outputs.qc?.value })` — get specific outputs",
51
+ inputSchema: {
52
+ projectId: z.string().describe("Project ID"),
53
+ blockId: z.string().describe("Block ID"),
54
+ transform: z
55
+ .string()
56
+ .optional()
57
+ .describe(
58
+ "JS expression evaluated server-side against full block state. " +
59
+ "Available variables: `data` (block args), `outputs` (raw outputs object). " +
60
+ "Omit for default concise summary.",
61
+ ),
62
+ transformTimeout: z
63
+ .number()
64
+ .optional()
65
+ .default(5000)
66
+ .describe("Timeout in ms for transform evaluation (default 5000)."),
67
+ },
68
+ },
69
+ async ({ projectId, blockId, transform, transformTimeout }) => {
70
+ const project = await ctx.getOpenedProject(projectId);
71
+ const state = await project.getBlockState(blockId).getValue();
72
+ const data = deriveDataFromStorage(state.blockStorage);
73
+ if (transform) {
74
+ try {
75
+ const result = await safeEval(
76
+ transform,
77
+ { data, outputs: state.outputs },
78
+ transformTimeout,
79
+ );
80
+ return textResult(result);
81
+ } catch (e: unknown) {
82
+ return errorResult(
83
+ `Transform failed: ${e instanceof Error ? e.message : String(e)}`,
84
+ "Check your JS expression syntax. Available variables: data, outputs.",
85
+ );
86
+ }
87
+ }
88
+ return textResult({
89
+ data,
90
+ outputs: summarizeOutputs(state.outputs as Record<string, unknown> | undefined),
91
+ });
92
+ },
93
+ );
94
+
95
+ server.registerTool(
96
+ "set_block_data",
97
+ {
98
+ description: "Set the user-facing data of a block (triggers args derivation and staging)",
99
+ inputSchema: {
100
+ projectId: z.string().describe("Project ID"),
101
+ blockId: z.string().describe("Block ID"),
102
+ data: z.record(z.unknown()).describe("Block data object"),
103
+ },
104
+ },
105
+ async ({ projectId, blockId, data }) => {
106
+ const project = await ctx.getOpenedProject(projectId);
107
+ await project.mutateBlockStorage(
108
+ blockId,
109
+ { operation: "update-block-data", value: data },
110
+ ctx.getAuthorMarker(),
111
+ );
112
+ return textResult({ ok: true });
113
+ },
114
+ );
115
+ }
@@ -0,0 +1,222 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { BlockPackSpecAny } from "@milaboratories/pl-middle-layer";
3
+ import { z } from "zod";
4
+ import type { ToolContext } from "./types";
5
+ import { errorResult, textResult } from "./types";
6
+
7
+ export function registerBlockTools(server: McpServer, ctx: ToolContext): void {
8
+ server.registerTool(
9
+ "add_block",
10
+ {
11
+ description:
12
+ "Add a block to an opened project. Spec can be from-registry-v2 (for published blocks) or dev-v2 (for local dev blocks).",
13
+ inputSchema: {
14
+ projectId: z.string().describe("Project ID (must be opened)"),
15
+ label: z.string().describe("Block label"),
16
+ spec: z
17
+ .union([
18
+ z.object({
19
+ type: z.literal("from-registry-v2"),
20
+ registryUrl: z.string().describe("Registry URL"),
21
+ id: z.object({
22
+ organization: z.string(),
23
+ name: z.string(),
24
+ version: z.string(),
25
+ }),
26
+ }),
27
+ z.object({
28
+ type: z.literal("dev-v2"),
29
+ folder: z.string().describe("Path to block folder"),
30
+ }),
31
+ ])
32
+ .describe("Block pack specification"),
33
+ },
34
+ },
35
+ async ({ projectId, label, spec }) => {
36
+ const project = await ctx.getOpenedProject(projectId);
37
+ const blockId = await project.addBlock(
38
+ label,
39
+ spec as BlockPackSpecAny,
40
+ undefined,
41
+ ctx.getAuthorMarker(),
42
+ );
43
+ return textResult({ blockId });
44
+ },
45
+ );
46
+
47
+ server.registerTool(
48
+ "update_block",
49
+ {
50
+ description:
51
+ "Update an existing block's pack (reload from registry or dev folder). Use after rebuilding a dev block to pick up changes without removing/re-adding it.",
52
+ inputSchema: {
53
+ projectId: z.string().describe("Project ID (must be opened)"),
54
+ blockId: z.string().describe("Block ID to update"),
55
+ spec: z
56
+ .union([
57
+ z.object({
58
+ type: z.literal("from-registry-v2"),
59
+ registryUrl: z.string().describe("Registry URL"),
60
+ id: z.object({
61
+ organization: z.string(),
62
+ name: z.string(),
63
+ version: z.string(),
64
+ }),
65
+ }),
66
+ z.object({
67
+ type: z.literal("dev-v2"),
68
+ folder: z.string().describe("Path to block folder"),
69
+ }),
70
+ ])
71
+ .describe("Block pack specification"),
72
+ resetArgs: z
73
+ .boolean()
74
+ .optional()
75
+ .describe("Reset block arguments to initial values (default: false)"),
76
+ },
77
+ },
78
+ async ({ projectId, blockId, spec, resetArgs }) => {
79
+ const project = await ctx.getOpenedProject(projectId);
80
+ await project.updateBlockPack(
81
+ blockId,
82
+ spec as BlockPackSpecAny,
83
+ resetArgs ?? false,
84
+ ctx.getAuthorMarker(),
85
+ );
86
+ return textResult({ ok: true });
87
+ },
88
+ );
89
+
90
+ server.registerTool(
91
+ "remove_block",
92
+ {
93
+ description: "Remove a block from an opened project",
94
+ inputSchema: {
95
+ projectId: z.string().describe("Project ID"),
96
+ blockId: z.string().describe("Block ID to remove"),
97
+ },
98
+ },
99
+ async ({ projectId, blockId }) => {
100
+ const project = await ctx.getOpenedProject(projectId);
101
+ await project.deleteBlock(blockId, ctx.getAuthorMarker());
102
+ return textResult({ ok: true });
103
+ },
104
+ );
105
+
106
+ server.registerTool(
107
+ "run_block",
108
+ {
109
+ description: "Run a block. Stale upstream blocks are started automatically.",
110
+ inputSchema: {
111
+ projectId: z.string().describe("Project ID"),
112
+ blockId: z.string().describe("Block ID to run"),
113
+ },
114
+ },
115
+ async ({ projectId, blockId }) => {
116
+ const project = await ctx.getOpenedProject(projectId);
117
+ await project.runBlock(blockId);
118
+ return textResult({ ok: true });
119
+ },
120
+ );
121
+
122
+ server.registerTool(
123
+ "stop_block",
124
+ {
125
+ description: "Stop a running block",
126
+ inputSchema: {
127
+ projectId: z.string().describe("Project ID"),
128
+ blockId: z.string().describe("Block ID to stop"),
129
+ },
130
+ },
131
+ async ({ projectId, blockId }) => {
132
+ const project = await ctx.getOpenedProject(projectId);
133
+ await project.stopBlock(blockId);
134
+ return textResult({ ok: true });
135
+ },
136
+ );
137
+
138
+ server.registerTool(
139
+ "reorder_blocks",
140
+ {
141
+ description: "Reorder blocks in a project. Must provide ALL block IDs in the desired order.",
142
+ inputSchema: {
143
+ projectId: z.string().describe("Project ID"),
144
+ blockIds: z.array(z.string()).describe("All block IDs in the desired order"),
145
+ },
146
+ },
147
+ async ({ projectId, blockIds }) => {
148
+ const project = await ctx.getOpenedProject(projectId);
149
+ await project.reorderBlocks(blockIds);
150
+ return textResult({ ok: true });
151
+ },
152
+ );
153
+
154
+ server.registerTool(
155
+ "list_available_blocks",
156
+ {
157
+ description:
158
+ "List available blocks from configured registries. Optional query to filter by name.",
159
+ inputSchema: {
160
+ query: z
161
+ .string()
162
+ .optional()
163
+ .describe("Filter blocks by name (case-insensitive substring match)"),
164
+ },
165
+ },
166
+ async ({ query }) => {
167
+ if (!ctx.callbacks.listAvailableBlocks) {
168
+ return errorResult(
169
+ "Block registry is not available.",
170
+ "This usually means the desktop app encounters a problem connecting block registry. Check Settings > Override Main Registry find which registry really in use and check connection.",
171
+ );
172
+ }
173
+ const blocks = await ctx.callbacks.listAvailableBlocks(query);
174
+ return textResult(blocks);
175
+ },
176
+ );
177
+
178
+ server.registerTool(
179
+ "get_block_info",
180
+ {
181
+ description:
182
+ "Get detailed info about a specific block package from the registry. Use list_available_blocks first to find the block.",
183
+ inputSchema: {
184
+ registryUrl: z.string().describe("Registry URL (from list_available_blocks)"),
185
+ organization: z.string().describe("Organization name"),
186
+ name: z.string().describe("Block package name"),
187
+ version: z.string().describe("Block version"),
188
+ },
189
+ },
190
+ async ({ registryUrl, organization, name, version }) => {
191
+ if (!ctx.callbacks.getBlockInfo) {
192
+ return errorResult(
193
+ "Block info is not available in this environment.",
194
+ 'Maybe the name of the block was written incerrectly. Use list_available_blocks to browse blocks instead. Or ask user to check "Additional Registries" in Settings panel',
195
+ );
196
+ }
197
+ const info = await ctx.callbacks.getBlockInfo(registryUrl, organization, name, version);
198
+ return textResult(info);
199
+ },
200
+ );
201
+
202
+ server.registerTool(
203
+ "select_block",
204
+ {
205
+ description: "Navigate the desktop UI to show a specific block's interface",
206
+ inputSchema: {
207
+ projectId: z.string().describe("Project ID"),
208
+ blockId: z.string().describe("Block ID to display"),
209
+ },
210
+ },
211
+ async ({ projectId, blockId }) => {
212
+ if (!ctx.callbacks.selectBlock) {
213
+ return errorResult(
214
+ "Failed to select the block.",
215
+ "This feature requires server connected and open project. Use get_connection_status and list_projects to check. If there are no connection, use list_connections and ask user which should be used.",
216
+ );
217
+ }
218
+ await ctx.callbacks.selectBlock(projectId, blockId);
219
+ return textResult({ ok: true });
220
+ },
221
+ );
222
+ }
@@ -0,0 +1,63 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { ToolContext } from "./types";
4
+ import { errorResult, textResult } from "./types";
5
+
6
+ export function registerConnectionTools(server: McpServer, ctx: ToolContext): void {
7
+ server.registerTool(
8
+ "get_connection_status",
9
+ { description: "Get current server connection status" },
10
+ async () => {
11
+ if (!ctx.callbacks.getConnectionStatus) {
12
+ return textResult({ connected: !!ctx.getMl() });
13
+ }
14
+ return textResult(await ctx.callbacks.getConnectionStatus());
15
+ },
16
+ );
17
+
18
+ server.registerTool(
19
+ "list_connections",
20
+ { description: "List saved server connections" },
21
+ async () => {
22
+ if (!ctx.callbacks.listConnections) {
23
+ return errorResult(
24
+ "Connection management is not available.",
25
+ "The desktop app integration may not support this feature.",
26
+ );
27
+ }
28
+ return textResult(await ctx.callbacks.listConnections());
29
+ },
30
+ );
31
+
32
+ server.registerTool(
33
+ "connect_to_server",
34
+ {
35
+ description: "Connect to a Platforma server. Use list_connections to see saved servers.",
36
+ inputSchema: {
37
+ addr: z.string().describe("Server address (e.g. https://pl6.demo2.platforma.bio:6346)"),
38
+ login: z.string().describe("Username"),
39
+ password: z.string().optional().describe("Password (uses saved token if omitted)"),
40
+ },
41
+ },
42
+ async ({ addr, login, password }) => {
43
+ if (!ctx.callbacks.connectToServer) {
44
+ return errorResult(
45
+ "Failed to connect to Platforma Server.",
46
+ "Check that provided URL is available and accepts connecitons.",
47
+ );
48
+ }
49
+ return textResult(await ctx.callbacks.connectToServer(addr, login, password));
50
+ },
51
+ );
52
+
53
+ server.registerTool("disconnect", { description: "Disconnect from current server" }, async () => {
54
+ if (!ctx.callbacks.disconnect) {
55
+ return errorResult(
56
+ "Failed to disconnect.",
57
+ "More likely it's because connection is already closed. Could check it with 'get_connection_status' tool.",
58
+ );
59
+ }
60
+ await ctx.callbacks.disconnect();
61
+ return textResult({ ok: true });
62
+ });
63
+ }