@nodetool-ai/websocket 0.7.0-rc.11
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/agent/agent-runtime.d.ts +47 -0
- package/dist/agent/agent-runtime.d.ts.map +1 -0
- package/dist/agent/agent-runtime.js +577 -0
- package/dist/agent/agent-runtime.js.map +1 -0
- package/dist/agent/codex-agent.d.ts +41 -0
- package/dist/agent/codex-agent.d.ts.map +1 -0
- package/dist/agent/codex-agent.js +334 -0
- package/dist/agent/codex-agent.js.map +1 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +51 -0
- package/dist/agent/llm-agent.d.ts.map +1 -0
- package/dist/agent/llm-agent.js +526 -0
- package/dist/agent/llm-agent.js.map +1 -0
- package/dist/agent/mcp-tool-server.d.ts +33 -0
- package/dist/agent/mcp-tool-server.d.ts.map +1 -0
- package/dist/agent/mcp-tool-server.js +292 -0
- package/dist/agent/mcp-tool-server.js.map +1 -0
- package/dist/agent/opencode-agent.d.ts +32 -0
- package/dist/agent/opencode-agent.d.ts.map +1 -0
- package/dist/agent/opencode-agent.js +332 -0
- package/dist/agent/opencode-agent.js.map +1 -0
- package/dist/agent/pi-agent.d.ts +42 -0
- package/dist/agent/pi-agent.d.ts.map +1 -0
- package/dist/agent/pi-agent.js +429 -0
- package/dist/agent/pi-agent.js.map +1 -0
- package/dist/agent/sdk-provider.d.ts +46 -0
- package/dist/agent/sdk-provider.d.ts.map +1 -0
- package/dist/agent/sdk-provider.js +50 -0
- package/dist/agent/sdk-provider.js.map +1 -0
- package/dist/agent/socket-route.d.ts +11 -0
- package/dist/agent/socket-route.d.ts.map +1 -0
- package/dist/agent/socket-route.js +262 -0
- package/dist/agent/socket-route.js.map +1 -0
- package/dist/agent/transport.d.ts +39 -0
- package/dist/agent/transport.d.ts.map +1 -0
- package/dist/agent/transport.js +17 -0
- package/dist/agent/transport.js.map +1 -0
- package/dist/agent/types.d.ts +11 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +10 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/collection-api.d.ts +14 -0
- package/dist/collection-api.d.ts.map +1 -0
- package/dist/collection-api.js +75 -0
- package/dist/collection-api.js.map +1 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/file-api.d.ts +12 -0
- package/dist/file-api.d.ts.map +1 -0
- package/dist/file-api.js +76 -0
- package/dist/file-api.js.map +1 -0
- package/dist/http-api.d.ts +94 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +1472 -0
- package/dist/http-api.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +10 -0
- package/dist/lib/asset-paths.d.ts.map +1 -0
- package/dist/lib/asset-paths.js +45 -0
- package/dist/lib/asset-paths.js.map +1 -0
- package/dist/lib/bridge.d.ts +8 -0
- package/dist/lib/bridge.d.ts.map +1 -0
- package/dist/lib/bridge.js +70 -0
- package/dist/lib/bridge.js.map +1 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +842 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/models-api.d.ts +12 -0
- package/dist/models-api.d.ts.map +1 -0
- package/dist/models-api.js +730 -0
- package/dist/models-api.js.map +1 -0
- package/dist/oauth-api.d.ts +28 -0
- package/dist/oauth-api.d.ts.map +1 -0
- package/dist/oauth-api.js +681 -0
- package/dist/oauth-api.js.map +1 -0
- package/dist/openai-api.d.ts +63 -0
- package/dist/openai-api.d.ts.map +1 -0
- package/dist/openai-api.js +359 -0
- package/dist/openai-api.js.map +1 -0
- package/dist/plugins/websocket.d.ts +14 -0
- package/dist/plugins/websocket.d.ts.map +1 -0
- package/dist/plugins/websocket.js +431 -0
- package/dist/plugins/websocket.js.map +1 -0
- package/dist/resolve-media-urls.d.ts +19 -0
- package/dist/resolve-media-urls.d.ts.map +1 -0
- package/dist/resolve-media-urls.js +109 -0
- package/dist/resolve-media-urls.js.map +1 -0
- package/dist/routes/assets.d.ts +22 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +168 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/collections.d.ts +16 -0
- package/dist/routes/collections.d.ts.map +1 -0
- package/dist/routes/collections.js +32 -0
- package/dist/routes/collections.js.map +1 -0
- package/dist/routes/files.d.ts +12 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +11 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +41 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/jobs.d.ts +16 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +25 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/nodes.d.ts +8 -0
- package/dist/routes/nodes.d.ts.map +1 -0
- package/dist/routes/nodes.js +124 -0
- package/dist/routes/nodes.js.map +1 -0
- package/dist/routes/oauth.d.ts +8 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +19 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/openai.d.ts +8 -0
- package/dist/routes/openai.d.ts.map +1 -0
- package/dist/routes/openai.js +20 -0
- package/dist/routes/openai.js.map +1 -0
- package/dist/routes/storage.d.ts +12 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +13 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/workflows.d.ts +18 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +25 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/routes/workspace.d.ts +15 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +24 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/screenshot-server.d.ts +21 -0
- package/dist/screenshot-server.d.ts.map +1 -0
- package/dist/screenshot-server.js +222 -0
- package/dist/screenshot-server.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +783 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-registry.d.ts +29 -0
- package/dist/settings-registry.d.ts.map +1 -0
- package/dist/settings-registry.js +117 -0
- package/dist/settings-registry.js.map +1 -0
- package/dist/storage-api.d.ts +5 -0
- package/dist/storage-api.d.ts.map +1 -0
- package/dist/storage-api.js +221 -0
- package/dist/storage-api.js.map +1 -0
- package/dist/test-ui-server.d.ts +19 -0
- package/dist/test-ui-server.d.ts.map +1 -0
- package/dist/test-ui-server.js +1256 -0
- package/dist/test-ui-server.js.map +1 -0
- package/dist/trpc/context.d.ts +21 -0
- package/dist/trpc/context.d.ts.map +1 -0
- package/dist/trpc/context.js +10 -0
- package/dist/trpc/context.js.map +1 -0
- package/dist/trpc/error-formatter.d.ts +18 -0
- package/dist/trpc/error-formatter.d.ts.map +1 -0
- package/dist/trpc/error-formatter.js +41 -0
- package/dist/trpc/error-formatter.js.map +1 -0
- package/dist/trpc/index.d.ts +18 -0
- package/dist/trpc/index.d.ts.map +1 -0
- package/dist/trpc/index.js +13 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/middleware.d.ts +8 -0
- package/dist/trpc/middleware.d.ts.map +1 -0
- package/dist/trpc/middleware.js +12 -0
- package/dist/trpc/middleware.js.map +1 -0
- package/dist/trpc/router.d.ts +2543 -0
- package/dist/trpc/router.d.ts.map +1 -0
- package/dist/trpc/router.js +43 -0
- package/dist/trpc/router.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +229 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -0
- package/dist/trpc/routers/assets.js +263 -0
- package/dist/trpc/routers/assets.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +88 -0
- package/dist/trpc/routers/collections.d.ts.map +1 -0
- package/dist/trpc/routers/collections.js +166 -0
- package/dist/trpc/routers/collections.js.map +1 -0
- package/dist/trpc/routers/costs.d.ts +129 -0
- package/dist/trpc/routers/costs.d.ts.map +1 -0
- package/dist/trpc/routers/costs.js +95 -0
- package/dist/trpc/routers/costs.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +34 -0
- package/dist/trpc/routers/files.d.ts.map +1 -0
- package/dist/trpc/routers/files.js +92 -0
- package/dist/trpc/routers/files.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +92 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -0
- package/dist/trpc/routers/jobs.js +100 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +69 -0
- package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
- package/dist/trpc/routers/mcp-config.js +290 -0
- package/dist/trpc/routers/mcp-config.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +121 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -0
- package/dist/trpc/routers/messages.js +113 -0
- package/dist/trpc/routers/messages.js.map +1 -0
- package/dist/trpc/routers/models.d.ts +748 -0
- package/dist/trpc/routers/models.d.ts.map +1 -0
- package/dist/trpc/routers/models.js +1107 -0
- package/dist/trpc/routers/models.js.map +1 -0
- package/dist/trpc/routers/nodes.d.ts +48 -0
- package/dist/trpc/routers/nodes.d.ts.map +1 -0
- package/dist/trpc/routers/nodes.js +57 -0
- package/dist/trpc/routers/nodes.js.map +1 -0
- package/dist/trpc/routers/sandboxes.d.ts +64 -0
- package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
- package/dist/trpc/routers/sandboxes.js +327 -0
- package/dist/trpc/routers/sandboxes.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +113 -0
- package/dist/trpc/routers/settings.d.ts.map +1 -0
- package/dist/trpc/routers/settings.js +193 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +41 -0
- package/dist/trpc/routers/skills.d.ts.map +1 -0
- package/dist/trpc/routers/skills.js +194 -0
- package/dist/trpc/routers/skills.js.map +1 -0
- package/dist/trpc/routers/storage.d.ts +44 -0
- package/dist/trpc/routers/storage.d.ts.map +1 -0
- package/dist/trpc/routers/storage.js +153 -0
- package/dist/trpc/routers/storage.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +95 -0
- package/dist/trpc/routers/threads.d.ts.map +1 -0
- package/dist/trpc/routers/threads.js +150 -0
- package/dist/trpc/routers/threads.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +79 -0
- package/dist/trpc/routers/users.d.ts.map +1 -0
- package/dist/trpc/routers/users.js +125 -0
- package/dist/trpc/routers/users.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +681 -0
- package/dist/trpc/routers/workflows.d.ts.map +1 -0
- package/dist/trpc/routers/workflows.js +725 -0
- package/dist/trpc/routers/workflows.js.map +1 -0
- package/dist/trpc/routers/workspace.d.ts +127 -0
- package/dist/trpc/routers/workspace.d.ts.map +1 -0
- package/dist/trpc/routers/workspace.js +203 -0
- package/dist/trpc/routers/workspace.js.map +1 -0
- package/dist/unified-websocket-runner.d.ts +280 -0
- package/dist/unified-websocket-runner.d.ts.map +1 -0
- package/dist/unified-websocket-runner.js +3840 -0
- package/dist/unified-websocket-runner.js.map +1 -0
- package/dist/workspace-api.d.ts +14 -0
- package/dist/workspace-api.d.ts.map +1 -0
- package/dist/workspace-api.js +104 -0
- package/dist/workspace-api.js.map +1 -0
- package/dist/ws-adapter.d.ts +25 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +63 -0
- package/dist/ws-adapter.js.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) server for NodeTool.
|
|
3
|
+
*
|
|
4
|
+
* Exposes NodeTool workflows, assets, nodes, jobs, and collections
|
|
5
|
+
* as MCP tools using the official TypeScript MCP SDK.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
12
|
+
import { Workflow, Job, Asset } from "@nodetool-ai/models";
|
|
13
|
+
import { toAssetResponse, toJobResponse, toWorkflowResponse } from "./http-api.js";
|
|
14
|
+
import { uiToolSchemas } from "@nodetool-ai/protocol";
|
|
15
|
+
import { NodeRegistry, rankNodeMetadata } from "@nodetool-ai/node-sdk";
|
|
16
|
+
import { registerBaseNodes } from "@nodetool-ai/base-nodes";
|
|
17
|
+
import { registerElevenLabsNodes } from "@nodetool-ai/elevenlabs-nodes";
|
|
18
|
+
import { registerTransformersJsNodes } from "@nodetool-ai/transformers-js-nodes";
|
|
19
|
+
import { registerFalNodes } from "@nodetool-ai/fal-nodes";
|
|
20
|
+
import { registerKieNodes } from "@nodetool-ai/kie-nodes";
|
|
21
|
+
import { registerReplicateNodes } from "@nodetool-ai/replicate-nodes";
|
|
22
|
+
import { PythonNodeExecutor, PythonStdioBridge } from "@nodetool-ai/runtime";
|
|
23
|
+
import { WorkflowRunner } from "@nodetool-ai/kernel";
|
|
24
|
+
const log = createLogger("nodetool.websocket.mcp-server");
|
|
25
|
+
const GLOBAL_FRONTEND_SESSION_ID = "global-mcp";
|
|
26
|
+
let activeFrontendTransport = null;
|
|
27
|
+
let runtimeEnvironmentPromise = null;
|
|
28
|
+
async function getUnifiedNodeMetadata(options) {
|
|
29
|
+
if (options?.registry) {
|
|
30
|
+
return options.registry
|
|
31
|
+
.listMetadata()
|
|
32
|
+
.sort((a, b) => a.node_type.localeCompare(b.node_type));
|
|
33
|
+
}
|
|
34
|
+
const runtime = await getRuntimeEnvironment(options);
|
|
35
|
+
return runtime.registry
|
|
36
|
+
.listMetadata()
|
|
37
|
+
.sort((a, b) => a.node_type.localeCompare(b.node_type));
|
|
38
|
+
}
|
|
39
|
+
function getRuntimeEnvironment(options) {
|
|
40
|
+
if (!runtimeEnvironmentPromise) {
|
|
41
|
+
runtimeEnvironmentPromise = (async () => {
|
|
42
|
+
const registry = new NodeRegistry();
|
|
43
|
+
registry.loadPythonMetadata({
|
|
44
|
+
roots: options?.metadataRoots,
|
|
45
|
+
maxDepth: options?.metadataMaxDepth ?? 8
|
|
46
|
+
});
|
|
47
|
+
registerBaseNodes(registry);
|
|
48
|
+
registerElevenLabsNodes(registry);
|
|
49
|
+
registerTransformersJsNodes(registry);
|
|
50
|
+
registerFalNodes(registry);
|
|
51
|
+
registerKieNodes(registry);
|
|
52
|
+
registerReplicateNodes(registry);
|
|
53
|
+
const pythonBridge = new PythonStdioBridge({
|
|
54
|
+
workerArgs: process.env["NODETOOL_WORKER_NAMESPACES"]
|
|
55
|
+
? ["--namespaces", process.env["NODETOOL_WORKER_NAMESPACES"]]
|
|
56
|
+
: []
|
|
57
|
+
});
|
|
58
|
+
const logPythonBridgeDiagnostics = (context) => {
|
|
59
|
+
const loadErrors = pythonBridge.getLoadErrors?.() ?? [];
|
|
60
|
+
if (loadErrors.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
log.warn(`MCP Python bridge ${context} with ${loadErrors.length} load error(s)`);
|
|
63
|
+
for (const entry of loadErrors.slice(0, 10)) {
|
|
64
|
+
log.warn(`[python-worker][load-error] ${entry.module} (${entry.phase}): ${entry.error}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
let pythonBridgeReady = false;
|
|
68
|
+
pythonBridge.on("stderr", (msg) => {
|
|
69
|
+
for (const line of msg.split("\n")) {
|
|
70
|
+
if (line.trim())
|
|
71
|
+
log.debug(`[python-worker] ${line}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
pythonBridge.on("exit", (code) => {
|
|
75
|
+
log.warn(`MCP Python worker exited with code ${code}`);
|
|
76
|
+
pythonBridgeReady = false;
|
|
77
|
+
});
|
|
78
|
+
const ensurePythonBridge = async () => {
|
|
79
|
+
log.info("MCP lazily starting Python bridge");
|
|
80
|
+
try {
|
|
81
|
+
await pythonBridge.ensureConnected();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
log.warn("MCP Python bridge lazy start failed", err instanceof Error ? err : new Error(String(err)));
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
pythonBridgeReady = true;
|
|
88
|
+
log.info("MCP Python bridge lazy start completed");
|
|
89
|
+
const meta = pythonBridge.getNodeMetadata();
|
|
90
|
+
log.info(`MCP Python bridge connected — ${meta.length} Python nodes`);
|
|
91
|
+
pythonBridge
|
|
92
|
+
.getWorkerStatus?.()
|
|
93
|
+
?.then((status) => {
|
|
94
|
+
log.info("MCP Python bridge status", {
|
|
95
|
+
protocol_version: status.protocol_version,
|
|
96
|
+
node_count: status.node_count,
|
|
97
|
+
provider_count: status.provider_count,
|
|
98
|
+
namespaces: status.namespaces,
|
|
99
|
+
transport: status.transport,
|
|
100
|
+
max_frame_size: status.max_frame_size,
|
|
101
|
+
load_error_count: status.load_errors.length
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
.catch((err) => {
|
|
105
|
+
log.warn("MCP failed to fetch Python bridge status", err instanceof Error ? err : new Error(String(err)));
|
|
106
|
+
});
|
|
107
|
+
logPythonBridgeDiagnostics("connected");
|
|
108
|
+
};
|
|
109
|
+
const resolveExecutor = (node) => {
|
|
110
|
+
if (registry.has(node.type)) {
|
|
111
|
+
return registry.resolve(node);
|
|
112
|
+
}
|
|
113
|
+
if (pythonBridgeReady && pythonBridge.hasNodeType(node.type)) {
|
|
114
|
+
const meta = pythonBridge
|
|
115
|
+
.getNodeMetadata()
|
|
116
|
+
.find((n) => n.node_type === node.type);
|
|
117
|
+
const nodeRec = node;
|
|
118
|
+
const props = (nodeRec.properties ?? nodeRec.data ?? {});
|
|
119
|
+
return new PythonNodeExecutor(pythonBridge, node.type, props, Object.fromEntries((meta?.outputs ?? []).map((o) => [o.name, o.type.type])), meta?.required_settings ?? []);
|
|
120
|
+
}
|
|
121
|
+
if (registry.getMetadata(node.type) && !registry.has(node.type)) {
|
|
122
|
+
const stderrSummary = pythonBridge.getRecentStderrSummary?.() ?? null;
|
|
123
|
+
const loadErrors = pythonBridge.getLoadErrors?.() ?? [];
|
|
124
|
+
const matchingLoadError = loadErrors.find((entry) => entry.module.includes(node.type) ||
|
|
125
|
+
node.type.startsWith(entry.module.split(".").slice(2).join(".")));
|
|
126
|
+
throw new Error(pythonBridgeReady
|
|
127
|
+
? `Python node "${node.type}" cannot execute: it is declared in metadata but was not loaded by the Python worker.${matchingLoadError ? ` Load error: ${matchingLoadError.module}: ${matchingLoadError.error}.` : stderrSummary ? ` Recent Python worker stderr: ${stderrSummary}` : " Check Python worker status/load errors for import failures."}`
|
|
128
|
+
: `Python node "${node.type}" cannot execute: Python worker is not connected.${stderrSummary ? ` Recent Python worker stderr: ${stderrSummary}` : ""}`);
|
|
129
|
+
}
|
|
130
|
+
return registry.resolve(node);
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
registry,
|
|
134
|
+
pythonBridge,
|
|
135
|
+
ensurePythonBridge,
|
|
136
|
+
resolveExecutor
|
|
137
|
+
};
|
|
138
|
+
})();
|
|
139
|
+
}
|
|
140
|
+
return runtimeEnvironmentPromise;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create a configured MCP server with all NodeTool tools registered.
|
|
144
|
+
*/
|
|
145
|
+
export function createMcpServer(options) {
|
|
146
|
+
const server = new McpServer({
|
|
147
|
+
name: "NodeTool API Server",
|
|
148
|
+
version: "1.0.0"
|
|
149
|
+
});
|
|
150
|
+
for (const [toolName, schema] of Object.entries(uiToolSchemas)) {
|
|
151
|
+
server.tool(toolName, schema.description, schema.parameters, async (args) => {
|
|
152
|
+
const transport = activeFrontendTransport;
|
|
153
|
+
if (!transport || !transport.isAlive) {
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
error: "No active NodeTool renderer is connected for frontend UI tools."
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
isError: true
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const result = await transport.executeTool(GLOBAL_FRONTEND_SESSION_ID, `mcp-ui-${toolName}-${crypto.randomUUID()}`, toolName, args);
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: typeof result === "string"
|
|
173
|
+
? result
|
|
174
|
+
: JSON.stringify(result ?? null)
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: JSON.stringify({ error: String(err) })
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
isError: true
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// ── Workflow tools ──────────────────────────────────────────────
|
|
193
|
+
server.tool("list_workflows", "List workflows with flexible filtering.", {
|
|
194
|
+
limit: z
|
|
195
|
+
.number()
|
|
196
|
+
.optional()
|
|
197
|
+
.default(100)
|
|
198
|
+
.describe("Maximum number of workflows to return"),
|
|
199
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
200
|
+
}, async ({ limit, user_id }) => {
|
|
201
|
+
try {
|
|
202
|
+
const [workflows, next] = await Workflow.paginate(user_id, { limit });
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: JSON.stringify({
|
|
208
|
+
workflows: workflows.map((workflow) => toWorkflowResponse(workflow)),
|
|
209
|
+
next: next || null
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: JSON.stringify({ error: String(err) })
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
isError: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
server.tool("get_workflow", "Get detailed information about a specific workflow.", {
|
|
228
|
+
workflow_id: z.string().describe("The workflow ID"),
|
|
229
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
230
|
+
}, async ({ workflow_id, user_id }) => {
|
|
231
|
+
try {
|
|
232
|
+
const workflow = await Workflow.find(user_id, workflow_id);
|
|
233
|
+
if (!workflow) {
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: "text",
|
|
238
|
+
text: JSON.stringify({ error: "Workflow not found" })
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
isError: true
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: JSON.stringify(toWorkflowResponse(workflow))
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: JSON.stringify({ error: String(err) })
|
|
259
|
+
}
|
|
260
|
+
],
|
|
261
|
+
isError: true
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
server.tool("run_workflow", "Run a workflow on the backend, optionally passing parameters.", {
|
|
266
|
+
workflow_id: z.string().describe("Workflow id to target."),
|
|
267
|
+
params: z
|
|
268
|
+
.record(z.string(), z.unknown())
|
|
269
|
+
.optional()
|
|
270
|
+
.default({})
|
|
271
|
+
.describe("Optional workflow run parameters."),
|
|
272
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
273
|
+
}, async ({ workflow_id, params, user_id }) => {
|
|
274
|
+
try {
|
|
275
|
+
const workflow = await Workflow.find(user_id, workflow_id);
|
|
276
|
+
if (!workflow) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: JSON.stringify({ error: "Workflow not found" })
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
isError: true
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const runMode = workflow.run_mode ?? "workflow";
|
|
288
|
+
if (runMode !== "workflow") {
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: JSON.stringify({
|
|
294
|
+
error: `Workflow run mode "${runMode}" is not supported by the backend MCP runner.`
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
],
|
|
298
|
+
isError: true
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const graph = workflow.getGraph();
|
|
302
|
+
const runnableGraph = {
|
|
303
|
+
nodes: graph.nodes.map((node) => {
|
|
304
|
+
const record = node;
|
|
305
|
+
return {
|
|
306
|
+
...record,
|
|
307
|
+
id: String(record.id ?? ""),
|
|
308
|
+
type: String(record.type ?? ""),
|
|
309
|
+
properties: (record.properties ?? record.data ?? {})
|
|
310
|
+
};
|
|
311
|
+
}),
|
|
312
|
+
edges: graph.edges.map((edge) => {
|
|
313
|
+
const record = edge;
|
|
314
|
+
return {
|
|
315
|
+
...record,
|
|
316
|
+
id: typeof record.id === "string" || record.id == null
|
|
317
|
+
? record.id
|
|
318
|
+
: String(record.id),
|
|
319
|
+
source: String(record.source ?? ""),
|
|
320
|
+
target: String(record.target ?? ""),
|
|
321
|
+
sourceHandle: String(record.sourceHandle ?? ""),
|
|
322
|
+
targetHandle: String(record.targetHandle ?? ""),
|
|
323
|
+
edge_type: record.edge_type === "control" ? "control" : "data"
|
|
324
|
+
};
|
|
325
|
+
})
|
|
326
|
+
};
|
|
327
|
+
const runtime = await getRuntimeEnvironment(options);
|
|
328
|
+
const hasPythonNode = runnableGraph.nodes.some((node) => {
|
|
329
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
330
|
+
return (nodeType !== "" &&
|
|
331
|
+
Boolean(runtime.registry.getMetadata(nodeType)) &&
|
|
332
|
+
!runtime.registry.has(nodeType));
|
|
333
|
+
});
|
|
334
|
+
if (hasPythonNode) {
|
|
335
|
+
await runtime.ensurePythonBridge();
|
|
336
|
+
}
|
|
337
|
+
const job = await Job.create({
|
|
338
|
+
workflow_id,
|
|
339
|
+
user_id,
|
|
340
|
+
status: "running",
|
|
341
|
+
params,
|
|
342
|
+
graph: runnableGraph
|
|
343
|
+
});
|
|
344
|
+
const runner = new WorkflowRunner(job.id, {
|
|
345
|
+
resolveExecutor: (node) => runtime.resolveExecutor(node)
|
|
346
|
+
});
|
|
347
|
+
const result = await runner.run({ job_id: job.id, workflow_id, params }, runnableGraph);
|
|
348
|
+
if (result.status === "completed") {
|
|
349
|
+
job.markCompleted();
|
|
350
|
+
}
|
|
351
|
+
else if (result.status === "cancelled") {
|
|
352
|
+
job.markCancelled();
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
job.markFailed(result.error ?? "Workflow run failed");
|
|
356
|
+
}
|
|
357
|
+
await job.save();
|
|
358
|
+
return {
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: JSON.stringify({
|
|
363
|
+
job_id: job.id,
|
|
364
|
+
workflow_id,
|
|
365
|
+
status: result.status,
|
|
366
|
+
outputs: result.outputs,
|
|
367
|
+
error: result.error ?? null,
|
|
368
|
+
message_count: result.messages.length,
|
|
369
|
+
background: false
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
return {
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: "text",
|
|
380
|
+
text: JSON.stringify({ error: String(err) })
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
isError: true
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ── Asset tools ─────────────────────────────────────────────────
|
|
388
|
+
server.tool("list_assets", "List or search assets with flexible filtering options.", {
|
|
389
|
+
parent_id: z.string().optional().describe("Filter by parent asset ID"),
|
|
390
|
+
content_type: z.string().optional().describe("Filter by content type"),
|
|
391
|
+
limit: z
|
|
392
|
+
.number()
|
|
393
|
+
.optional()
|
|
394
|
+
.default(100)
|
|
395
|
+
.describe("Maximum number of assets to return"),
|
|
396
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
397
|
+
}, async ({ parent_id, content_type, limit, user_id }) => {
|
|
398
|
+
try {
|
|
399
|
+
const opts = { limit };
|
|
400
|
+
if (parent_id)
|
|
401
|
+
opts.parentId = parent_id;
|
|
402
|
+
if (content_type)
|
|
403
|
+
opts.contentType = content_type;
|
|
404
|
+
const [assets, next] = await Asset.paginate(user_id, opts);
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: JSON.stringify({
|
|
410
|
+
assets: assets.map((asset) => toAssetResponse(asset)),
|
|
411
|
+
next: next || null
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: JSON.stringify({ error: String(err) })
|
|
423
|
+
}
|
|
424
|
+
],
|
|
425
|
+
isError: true
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
server.tool("get_asset", "Get detailed information about a specific asset.", {
|
|
430
|
+
asset_id: z.string().describe("The asset ID"),
|
|
431
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
432
|
+
}, async ({ asset_id, user_id }) => {
|
|
433
|
+
try {
|
|
434
|
+
const asset = await Asset.find(user_id, asset_id);
|
|
435
|
+
if (!asset) {
|
|
436
|
+
return {
|
|
437
|
+
content: [
|
|
438
|
+
{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: JSON.stringify({ error: "Asset not found" })
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
isError: true
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: JSON.stringify(toAssetResponse(asset))
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
return {
|
|
457
|
+
content: [
|
|
458
|
+
{
|
|
459
|
+
type: "text",
|
|
460
|
+
text: JSON.stringify({ error: String(err) })
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
isError: true
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
// ── Node tools ──────────────────────────────────────────────────
|
|
468
|
+
server.tool("list_nodes", "List available nodes from installed packages.", {
|
|
469
|
+
namespace: z.string().optional().describe("Filter by namespace prefix"),
|
|
470
|
+
limit: z
|
|
471
|
+
.number()
|
|
472
|
+
.optional()
|
|
473
|
+
.default(200)
|
|
474
|
+
.describe("Maximum number of nodes to return")
|
|
475
|
+
}, async ({ namespace, limit }) => {
|
|
476
|
+
try {
|
|
477
|
+
let nodes = await getUnifiedNodeMetadata(options);
|
|
478
|
+
if (namespace) {
|
|
479
|
+
nodes = nodes.filter((n) => n.namespace.startsWith(namespace));
|
|
480
|
+
}
|
|
481
|
+
nodes.sort((a, b) => a.node_type.localeCompare(b.node_type));
|
|
482
|
+
const result = nodes.slice(0, limit).map((n) => ({
|
|
483
|
+
node_type: n.node_type,
|
|
484
|
+
title: n.title,
|
|
485
|
+
description: n.description,
|
|
486
|
+
namespace: n.namespace
|
|
487
|
+
}));
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: JSON.stringify({ error: String(err) })
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
isError: true
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
server.tool("search_nodes", "Search for nodes by name, description, or tags. Provider-specific nodes (openai.*, anthropic.*, etc.) are hidden by default; set include_provider_nodes:true to include them.", {
|
|
505
|
+
query: z.array(z.string()).describe("Search keywords"),
|
|
506
|
+
n_results: z.number().optional().default(10).describe("Maximum results"),
|
|
507
|
+
namespace: z
|
|
508
|
+
.string()
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("Optional namespace prefix to scope the search (e.g. 'nodetool.control')."),
|
|
511
|
+
include_provider_nodes: z
|
|
512
|
+
.boolean()
|
|
513
|
+
.optional()
|
|
514
|
+
.default(false)
|
|
515
|
+
.describe("Include provider-specific nodes. Default false — set true only when the user named a provider.")
|
|
516
|
+
}, async ({ query, n_results, namespace, include_provider_nodes }) => {
|
|
517
|
+
try {
|
|
518
|
+
const nodes = await getUnifiedNodeMetadata(options);
|
|
519
|
+
const ranked = rankNodeMetadata(nodes, query, {
|
|
520
|
+
includeProviderNodes: include_provider_nodes,
|
|
521
|
+
namespacePrefix: namespace
|
|
522
|
+
});
|
|
523
|
+
const matches = ranked.slice(0, n_results).map(({ meta, score }) => ({
|
|
524
|
+
node_type: meta.node_type,
|
|
525
|
+
title: meta.title,
|
|
526
|
+
description: meta.description,
|
|
527
|
+
namespace: meta.namespace,
|
|
528
|
+
score
|
|
529
|
+
}));
|
|
530
|
+
return {
|
|
531
|
+
content: [{ type: "text", text: JSON.stringify(matches) }]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
return {
|
|
536
|
+
content: [
|
|
537
|
+
{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: JSON.stringify({ error: String(err) })
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
isError: true
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
server.tool("get_node_info", "Get detailed metadata for a node type.", {
|
|
547
|
+
node_type: z.string().describe("The fully-qualified node type")
|
|
548
|
+
}, async ({ node_type }) => {
|
|
549
|
+
try {
|
|
550
|
+
const nodes = await getUnifiedNodeMetadata(options);
|
|
551
|
+
const node = nodes.find((candidate) => candidate.node_type === node_type);
|
|
552
|
+
if (!node) {
|
|
553
|
+
return {
|
|
554
|
+
content: [
|
|
555
|
+
{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: JSON.stringify({ error: "Node type not found" })
|
|
558
|
+
}
|
|
559
|
+
],
|
|
560
|
+
isError: true
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
content: [{ type: "text", text: JSON.stringify(node) }]
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
return {
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: JSON.stringify({ error: String(err) })
|
|
573
|
+
}
|
|
574
|
+
],
|
|
575
|
+
isError: true
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// ── Job tools ───────────────────────────────────────────────────
|
|
580
|
+
server.tool("list_jobs", "List jobs for user, optionally filtered by workflow.", {
|
|
581
|
+
workflow_id: z.string().optional().describe("Filter by workflow ID"),
|
|
582
|
+
limit: z
|
|
583
|
+
.number()
|
|
584
|
+
.optional()
|
|
585
|
+
.default(100)
|
|
586
|
+
.describe("Maximum number of jobs to return"),
|
|
587
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
588
|
+
}, async ({ workflow_id, limit, user_id }) => {
|
|
589
|
+
try {
|
|
590
|
+
const opts = { limit };
|
|
591
|
+
if (workflow_id)
|
|
592
|
+
opts.workflowId = workflow_id;
|
|
593
|
+
const [jobs, nextStartKey] = await Job.paginate(user_id, opts);
|
|
594
|
+
return {
|
|
595
|
+
content: [
|
|
596
|
+
{
|
|
597
|
+
type: "text",
|
|
598
|
+
text: JSON.stringify({
|
|
599
|
+
jobs: jobs.map((job) => toJobResponse(job)),
|
|
600
|
+
next_start_key: nextStartKey || null
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
]
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
return {
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: "text",
|
|
611
|
+
text: JSON.stringify({ error: String(err) })
|
|
612
|
+
}
|
|
613
|
+
],
|
|
614
|
+
isError: true
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
server.tool("get_job", "Get a job by ID for user.", {
|
|
619
|
+
job_id: z.string().describe("The job ID"),
|
|
620
|
+
user_id: z.string().optional().default("1").describe("User ID")
|
|
621
|
+
}, async ({ job_id, user_id }) => {
|
|
622
|
+
try {
|
|
623
|
+
const job = await Job.find(user_id, job_id);
|
|
624
|
+
if (!job) {
|
|
625
|
+
return {
|
|
626
|
+
content: [
|
|
627
|
+
{
|
|
628
|
+
type: "text",
|
|
629
|
+
text: JSON.stringify({ error: "Job not found" })
|
|
630
|
+
}
|
|
631
|
+
],
|
|
632
|
+
isError: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: JSON.stringify(toJobResponse(job))
|
|
640
|
+
}
|
|
641
|
+
]
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
return {
|
|
646
|
+
content: [
|
|
647
|
+
{
|
|
648
|
+
type: "text",
|
|
649
|
+
text: JSON.stringify({ error: String(err) })
|
|
650
|
+
}
|
|
651
|
+
],
|
|
652
|
+
isError: true
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
// ── Collection tools (sqlite-vec) ───────────────────────────────
|
|
657
|
+
server.tool("list_collections", "List all vector database collections.", {
|
|
658
|
+
limit: z
|
|
659
|
+
.number()
|
|
660
|
+
.optional()
|
|
661
|
+
.default(50)
|
|
662
|
+
.describe("Maximum collections to return")
|
|
663
|
+
}, async ({ limit }) => {
|
|
664
|
+
try {
|
|
665
|
+
const { getVecStore } = await import("@nodetool-ai/vectorstore");
|
|
666
|
+
const store = await getVecStore();
|
|
667
|
+
const collections = await store.listCollections();
|
|
668
|
+
const result = await Promise.all(collections.slice(0, limit).map(async (c) => {
|
|
669
|
+
const count = await c.count();
|
|
670
|
+
const metadata = c.metadata ?? {};
|
|
671
|
+
let workflowName = null;
|
|
672
|
+
const workflowId = metadata.workflow;
|
|
673
|
+
if (workflowId) {
|
|
674
|
+
const workflow = (await Workflow.get(workflowId));
|
|
675
|
+
if (workflow)
|
|
676
|
+
workflowName = workflow.name;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
name: c.name,
|
|
680
|
+
count,
|
|
681
|
+
metadata,
|
|
682
|
+
workflow_name: workflowName
|
|
683
|
+
};
|
|
684
|
+
}));
|
|
685
|
+
return {
|
|
686
|
+
content: [
|
|
687
|
+
{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: JSON.stringify({ collections: result, count: result.length })
|
|
690
|
+
}
|
|
691
|
+
]
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return {
|
|
696
|
+
content: [
|
|
697
|
+
{
|
|
698
|
+
type: "text",
|
|
699
|
+
text: JSON.stringify({ error: "Vector store not available" })
|
|
700
|
+
}
|
|
701
|
+
],
|
|
702
|
+
isError: true
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
server.tool("get_collection", "Get details about a specific collection.", {
|
|
707
|
+
name: z.string().describe("Collection name")
|
|
708
|
+
}, async ({ name }) => {
|
|
709
|
+
try {
|
|
710
|
+
const { getVecStore } = await import("@nodetool-ai/vectorstore");
|
|
711
|
+
const store = await getVecStore();
|
|
712
|
+
const collection = await store.getCollection({ name });
|
|
713
|
+
const count = await collection.count();
|
|
714
|
+
return {
|
|
715
|
+
content: [
|
|
716
|
+
{
|
|
717
|
+
type: "text",
|
|
718
|
+
text: JSON.stringify({
|
|
719
|
+
name: collection.name,
|
|
720
|
+
metadata: collection.metadata,
|
|
721
|
+
count
|
|
722
|
+
})
|
|
723
|
+
}
|
|
724
|
+
]
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: JSON.stringify({ error: "Vector store not available" })
|
|
733
|
+
}
|
|
734
|
+
],
|
|
735
|
+
isError: true
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
server.tool("query_collection", "Query a collection for similar documents using semantic search.", {
|
|
740
|
+
name: z.string().describe("Collection name"),
|
|
741
|
+
query_texts: z
|
|
742
|
+
.array(z.string())
|
|
743
|
+
.describe("Query texts for semantic search"),
|
|
744
|
+
n_results: z.number().optional().default(10).describe("Number of results")
|
|
745
|
+
}, async ({ name, query_texts, n_results }) => {
|
|
746
|
+
try {
|
|
747
|
+
const { getVecStore } = await import("@nodetool-ai/vectorstore");
|
|
748
|
+
const store = await getVecStore();
|
|
749
|
+
const collection = await store.getCollection({ name });
|
|
750
|
+
const results = await collection.query({
|
|
751
|
+
queryTexts: query_texts,
|
|
752
|
+
nResults: n_results
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
content: [{ type: "text", text: JSON.stringify(results) }]
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: JSON.stringify({ error: "Vector store not available" })
|
|
764
|
+
}
|
|
765
|
+
],
|
|
766
|
+
isError: true
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return server;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Create a stdio transport for CLI mode.
|
|
774
|
+
*/
|
|
775
|
+
export function createMcpStdioTransport() {
|
|
776
|
+
return new StdioServerTransport();
|
|
777
|
+
}
|
|
778
|
+
// Per-session transport map for stateful HTTP MCP
|
|
779
|
+
const sessionTransports = new Map();
|
|
780
|
+
export function setMcpFrontendTransport(transport) {
|
|
781
|
+
activeFrontendTransport = transport;
|
|
782
|
+
}
|
|
783
|
+
export function clearMcpFrontendTransport(transport) {
|
|
784
|
+
if (activeFrontendTransport === transport) {
|
|
785
|
+
activeFrontendTransport = null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export function getLocalMcpServerUrl() {
|
|
789
|
+
const port = Number(process.env["PORT"] ?? 7777);
|
|
790
|
+
const tlsEnabled = Boolean(process.env["TLS_CERT"] && process.env["TLS_KEY"]);
|
|
791
|
+
const protocol = tlsEnabled ? "https" : "http";
|
|
792
|
+
return `${protocol}://127.0.0.1:${port}/mcp`;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Handle an MCP HTTP request at the /mcp path.
|
|
796
|
+
* Uses WebStandardStreamableHTTPServerTransport for stateful sessions.
|
|
797
|
+
*/
|
|
798
|
+
export async function handleMcpHttpRequest(request, options) {
|
|
799
|
+
const url = new URL(request.url);
|
|
800
|
+
if (!url.pathname.startsWith("/mcp"))
|
|
801
|
+
return null;
|
|
802
|
+
const method = request.method;
|
|
803
|
+
if (method === "POST") {
|
|
804
|
+
// Check for existing session
|
|
805
|
+
const sessionId = request.headers.get("mcp-session-id");
|
|
806
|
+
if (sessionId && sessionTransports.has(sessionId)) {
|
|
807
|
+
const transport = sessionTransports.get(sessionId);
|
|
808
|
+
return transport.handleRequest(request);
|
|
809
|
+
}
|
|
810
|
+
// New session — create transport and server
|
|
811
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
812
|
+
enableJsonResponse: true,
|
|
813
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
814
|
+
onsessioninitialized: (id) => {
|
|
815
|
+
sessionTransports.set(id, transport);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
const server = createMcpServer(options);
|
|
819
|
+
await server.connect(transport);
|
|
820
|
+
return transport.handleRequest(request);
|
|
821
|
+
}
|
|
822
|
+
if (method === "GET") {
|
|
823
|
+
const sessionId = request.headers.get("mcp-session-id");
|
|
824
|
+
if (sessionId && sessionTransports.has(sessionId)) {
|
|
825
|
+
const transport = sessionTransports.get(sessionId);
|
|
826
|
+
return transport.handleRequest(request);
|
|
827
|
+
}
|
|
828
|
+
return new Response("Session not found", { status: 404 });
|
|
829
|
+
}
|
|
830
|
+
if (method === "DELETE") {
|
|
831
|
+
const sessionId = request.headers.get("mcp-session-id");
|
|
832
|
+
if (sessionId && sessionTransports.has(sessionId)) {
|
|
833
|
+
const transport = sessionTransports.get(sessionId);
|
|
834
|
+
const response = await transport.handleRequest(request);
|
|
835
|
+
sessionTransports.delete(sessionId);
|
|
836
|
+
return response;
|
|
837
|
+
}
|
|
838
|
+
return new Response("Session not found", { status: 404 });
|
|
839
|
+
}
|
|
840
|
+
return new Response("Method not allowed", { status: 405 });
|
|
841
|
+
}
|
|
842
|
+
//# sourceMappingURL=mcp-server.js.map
|