@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.
Files changed (261) hide show
  1. package/dist/agent/agent-runtime.d.ts +47 -0
  2. package/dist/agent/agent-runtime.d.ts.map +1 -0
  3. package/dist/agent/agent-runtime.js +577 -0
  4. package/dist/agent/agent-runtime.js.map +1 -0
  5. package/dist/agent/codex-agent.d.ts +41 -0
  6. package/dist/agent/codex-agent.d.ts.map +1 -0
  7. package/dist/agent/codex-agent.js +334 -0
  8. package/dist/agent/codex-agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +6 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +4 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/llm-agent.d.ts +51 -0
  14. package/dist/agent/llm-agent.d.ts.map +1 -0
  15. package/dist/agent/llm-agent.js +526 -0
  16. package/dist/agent/llm-agent.js.map +1 -0
  17. package/dist/agent/mcp-tool-server.d.ts +33 -0
  18. package/dist/agent/mcp-tool-server.d.ts.map +1 -0
  19. package/dist/agent/mcp-tool-server.js +292 -0
  20. package/dist/agent/mcp-tool-server.js.map +1 -0
  21. package/dist/agent/opencode-agent.d.ts +32 -0
  22. package/dist/agent/opencode-agent.d.ts.map +1 -0
  23. package/dist/agent/opencode-agent.js +332 -0
  24. package/dist/agent/opencode-agent.js.map +1 -0
  25. package/dist/agent/pi-agent.d.ts +42 -0
  26. package/dist/agent/pi-agent.d.ts.map +1 -0
  27. package/dist/agent/pi-agent.js +429 -0
  28. package/dist/agent/pi-agent.js.map +1 -0
  29. package/dist/agent/sdk-provider.d.ts +46 -0
  30. package/dist/agent/sdk-provider.d.ts.map +1 -0
  31. package/dist/agent/sdk-provider.js +50 -0
  32. package/dist/agent/sdk-provider.js.map +1 -0
  33. package/dist/agent/socket-route.d.ts +11 -0
  34. package/dist/agent/socket-route.d.ts.map +1 -0
  35. package/dist/agent/socket-route.js +262 -0
  36. package/dist/agent/socket-route.js.map +1 -0
  37. package/dist/agent/transport.d.ts +39 -0
  38. package/dist/agent/transport.d.ts.map +1 -0
  39. package/dist/agent/transport.js +17 -0
  40. package/dist/agent/transport.js.map +1 -0
  41. package/dist/agent/types.d.ts +11 -0
  42. package/dist/agent/types.d.ts.map +1 -0
  43. package/dist/agent/types.js +10 -0
  44. package/dist/agent/types.js.map +1 -0
  45. package/dist/collection-api.d.ts +14 -0
  46. package/dist/collection-api.d.ts.map +1 -0
  47. package/dist/collection-api.js +75 -0
  48. package/dist/collection-api.js.map +1 -0
  49. package/dist/error-codes.d.ts +3 -0
  50. package/dist/error-codes.d.ts.map +1 -0
  51. package/dist/error-codes.js +2 -0
  52. package/dist/error-codes.js.map +1 -0
  53. package/dist/file-api.d.ts +12 -0
  54. package/dist/file-api.d.ts.map +1 -0
  55. package/dist/file-api.js +76 -0
  56. package/dist/file-api.js.map +1 -0
  57. package/dist/http-api.d.ts +94 -0
  58. package/dist/http-api.d.ts.map +1 -0
  59. package/dist/http-api.js +1472 -0
  60. package/dist/http-api.js.map +1 -0
  61. package/dist/index.d.ts +6 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +6 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/lib/asset-paths.d.ts +10 -0
  66. package/dist/lib/asset-paths.d.ts.map +1 -0
  67. package/dist/lib/asset-paths.js +45 -0
  68. package/dist/lib/asset-paths.js.map +1 -0
  69. package/dist/lib/bridge.d.ts +8 -0
  70. package/dist/lib/bridge.d.ts.map +1 -0
  71. package/dist/lib/bridge.js +70 -0
  72. package/dist/lib/bridge.js.map +1 -0
  73. package/dist/mcp-server.d.ts +32 -0
  74. package/dist/mcp-server.d.ts.map +1 -0
  75. package/dist/mcp-server.js +842 -0
  76. package/dist/mcp-server.js.map +1 -0
  77. package/dist/models-api.d.ts +12 -0
  78. package/dist/models-api.d.ts.map +1 -0
  79. package/dist/models-api.js +730 -0
  80. package/dist/models-api.js.map +1 -0
  81. package/dist/oauth-api.d.ts +28 -0
  82. package/dist/oauth-api.d.ts.map +1 -0
  83. package/dist/oauth-api.js +681 -0
  84. package/dist/oauth-api.js.map +1 -0
  85. package/dist/openai-api.d.ts +63 -0
  86. package/dist/openai-api.d.ts.map +1 -0
  87. package/dist/openai-api.js +359 -0
  88. package/dist/openai-api.js.map +1 -0
  89. package/dist/plugins/websocket.d.ts +14 -0
  90. package/dist/plugins/websocket.d.ts.map +1 -0
  91. package/dist/plugins/websocket.js +431 -0
  92. package/dist/plugins/websocket.js.map +1 -0
  93. package/dist/resolve-media-urls.d.ts +19 -0
  94. package/dist/resolve-media-urls.d.ts.map +1 -0
  95. package/dist/resolve-media-urls.js +109 -0
  96. package/dist/resolve-media-urls.js.map +1 -0
  97. package/dist/routes/assets.d.ts +22 -0
  98. package/dist/routes/assets.d.ts.map +1 -0
  99. package/dist/routes/assets.js +168 -0
  100. package/dist/routes/assets.js.map +1 -0
  101. package/dist/routes/collections.d.ts +16 -0
  102. package/dist/routes/collections.d.ts.map +1 -0
  103. package/dist/routes/collections.js +32 -0
  104. package/dist/routes/collections.js.map +1 -0
  105. package/dist/routes/files.d.ts +12 -0
  106. package/dist/routes/files.d.ts.map +1 -0
  107. package/dist/routes/files.js +11 -0
  108. package/dist/routes/files.js.map +1 -0
  109. package/dist/routes/health.d.ts +4 -0
  110. package/dist/routes/health.d.ts.map +1 -0
  111. package/dist/routes/health.js +41 -0
  112. package/dist/routes/health.js.map +1 -0
  113. package/dist/routes/jobs.d.ts +16 -0
  114. package/dist/routes/jobs.d.ts.map +1 -0
  115. package/dist/routes/jobs.js +25 -0
  116. package/dist/routes/jobs.js.map +1 -0
  117. package/dist/routes/nodes.d.ts +8 -0
  118. package/dist/routes/nodes.d.ts.map +1 -0
  119. package/dist/routes/nodes.js +124 -0
  120. package/dist/routes/nodes.js.map +1 -0
  121. package/dist/routes/oauth.d.ts +8 -0
  122. package/dist/routes/oauth.d.ts.map +1 -0
  123. package/dist/routes/oauth.js +19 -0
  124. package/dist/routes/oauth.js.map +1 -0
  125. package/dist/routes/openai.d.ts +8 -0
  126. package/dist/routes/openai.d.ts.map +1 -0
  127. package/dist/routes/openai.js +20 -0
  128. package/dist/routes/openai.js.map +1 -0
  129. package/dist/routes/storage.d.ts +12 -0
  130. package/dist/routes/storage.d.ts.map +1 -0
  131. package/dist/routes/storage.js +13 -0
  132. package/dist/routes/storage.js.map +1 -0
  133. package/dist/routes/workflows.d.ts +18 -0
  134. package/dist/routes/workflows.d.ts.map +1 -0
  135. package/dist/routes/workflows.js +25 -0
  136. package/dist/routes/workflows.js.map +1 -0
  137. package/dist/routes/workspace.d.ts +15 -0
  138. package/dist/routes/workspace.d.ts.map +1 -0
  139. package/dist/routes/workspace.js +24 -0
  140. package/dist/routes/workspace.js.map +1 -0
  141. package/dist/screenshot-server.d.ts +21 -0
  142. package/dist/screenshot-server.d.ts.map +1 -0
  143. package/dist/screenshot-server.js +222 -0
  144. package/dist/screenshot-server.js.map +1 -0
  145. package/dist/server.d.ts +10 -0
  146. package/dist/server.d.ts.map +1 -0
  147. package/dist/server.js +783 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/settings-registry.d.ts +29 -0
  150. package/dist/settings-registry.d.ts.map +1 -0
  151. package/dist/settings-registry.js +117 -0
  152. package/dist/settings-registry.js.map +1 -0
  153. package/dist/storage-api.d.ts +5 -0
  154. package/dist/storage-api.d.ts.map +1 -0
  155. package/dist/storage-api.js +221 -0
  156. package/dist/storage-api.js.map +1 -0
  157. package/dist/test-ui-server.d.ts +19 -0
  158. package/dist/test-ui-server.d.ts.map +1 -0
  159. package/dist/test-ui-server.js +1256 -0
  160. package/dist/test-ui-server.js.map +1 -0
  161. package/dist/trpc/context.d.ts +21 -0
  162. package/dist/trpc/context.d.ts.map +1 -0
  163. package/dist/trpc/context.js +10 -0
  164. package/dist/trpc/context.js.map +1 -0
  165. package/dist/trpc/error-formatter.d.ts +18 -0
  166. package/dist/trpc/error-formatter.d.ts.map +1 -0
  167. package/dist/trpc/error-formatter.js +41 -0
  168. package/dist/trpc/error-formatter.js.map +1 -0
  169. package/dist/trpc/index.d.ts +18 -0
  170. package/dist/trpc/index.d.ts.map +1 -0
  171. package/dist/trpc/index.js +13 -0
  172. package/dist/trpc/index.js.map +1 -0
  173. package/dist/trpc/middleware.d.ts +8 -0
  174. package/dist/trpc/middleware.d.ts.map +1 -0
  175. package/dist/trpc/middleware.js +12 -0
  176. package/dist/trpc/middleware.js.map +1 -0
  177. package/dist/trpc/router.d.ts +2543 -0
  178. package/dist/trpc/router.d.ts.map +1 -0
  179. package/dist/trpc/router.js +43 -0
  180. package/dist/trpc/router.js.map +1 -0
  181. package/dist/trpc/routers/assets.d.ts +229 -0
  182. package/dist/trpc/routers/assets.d.ts.map +1 -0
  183. package/dist/trpc/routers/assets.js +263 -0
  184. package/dist/trpc/routers/assets.js.map +1 -0
  185. package/dist/trpc/routers/collections.d.ts +88 -0
  186. package/dist/trpc/routers/collections.d.ts.map +1 -0
  187. package/dist/trpc/routers/collections.js +166 -0
  188. package/dist/trpc/routers/collections.js.map +1 -0
  189. package/dist/trpc/routers/costs.d.ts +129 -0
  190. package/dist/trpc/routers/costs.d.ts.map +1 -0
  191. package/dist/trpc/routers/costs.js +95 -0
  192. package/dist/trpc/routers/costs.js.map +1 -0
  193. package/dist/trpc/routers/files.d.ts +34 -0
  194. package/dist/trpc/routers/files.d.ts.map +1 -0
  195. package/dist/trpc/routers/files.js +92 -0
  196. package/dist/trpc/routers/files.js.map +1 -0
  197. package/dist/trpc/routers/jobs.d.ts +92 -0
  198. package/dist/trpc/routers/jobs.d.ts.map +1 -0
  199. package/dist/trpc/routers/jobs.js +100 -0
  200. package/dist/trpc/routers/jobs.js.map +1 -0
  201. package/dist/trpc/routers/mcp-config.d.ts +69 -0
  202. package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
  203. package/dist/trpc/routers/mcp-config.js +290 -0
  204. package/dist/trpc/routers/mcp-config.js.map +1 -0
  205. package/dist/trpc/routers/messages.d.ts +121 -0
  206. package/dist/trpc/routers/messages.d.ts.map +1 -0
  207. package/dist/trpc/routers/messages.js +113 -0
  208. package/dist/trpc/routers/messages.js.map +1 -0
  209. package/dist/trpc/routers/models.d.ts +748 -0
  210. package/dist/trpc/routers/models.d.ts.map +1 -0
  211. package/dist/trpc/routers/models.js +1107 -0
  212. package/dist/trpc/routers/models.js.map +1 -0
  213. package/dist/trpc/routers/nodes.d.ts +48 -0
  214. package/dist/trpc/routers/nodes.d.ts.map +1 -0
  215. package/dist/trpc/routers/nodes.js +57 -0
  216. package/dist/trpc/routers/nodes.js.map +1 -0
  217. package/dist/trpc/routers/sandboxes.d.ts +64 -0
  218. package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
  219. package/dist/trpc/routers/sandboxes.js +327 -0
  220. package/dist/trpc/routers/sandboxes.js.map +1 -0
  221. package/dist/trpc/routers/settings.d.ts +113 -0
  222. package/dist/trpc/routers/settings.d.ts.map +1 -0
  223. package/dist/trpc/routers/settings.js +193 -0
  224. package/dist/trpc/routers/settings.js.map +1 -0
  225. package/dist/trpc/routers/skills.d.ts +41 -0
  226. package/dist/trpc/routers/skills.d.ts.map +1 -0
  227. package/dist/trpc/routers/skills.js +194 -0
  228. package/dist/trpc/routers/skills.js.map +1 -0
  229. package/dist/trpc/routers/storage.d.ts +44 -0
  230. package/dist/trpc/routers/storage.d.ts.map +1 -0
  231. package/dist/trpc/routers/storage.js +153 -0
  232. package/dist/trpc/routers/storage.js.map +1 -0
  233. package/dist/trpc/routers/threads.d.ts +95 -0
  234. package/dist/trpc/routers/threads.d.ts.map +1 -0
  235. package/dist/trpc/routers/threads.js +150 -0
  236. package/dist/trpc/routers/threads.js.map +1 -0
  237. package/dist/trpc/routers/users.d.ts +79 -0
  238. package/dist/trpc/routers/users.d.ts.map +1 -0
  239. package/dist/trpc/routers/users.js +125 -0
  240. package/dist/trpc/routers/users.js.map +1 -0
  241. package/dist/trpc/routers/workflows.d.ts +681 -0
  242. package/dist/trpc/routers/workflows.d.ts.map +1 -0
  243. package/dist/trpc/routers/workflows.js +725 -0
  244. package/dist/trpc/routers/workflows.js.map +1 -0
  245. package/dist/trpc/routers/workspace.d.ts +127 -0
  246. package/dist/trpc/routers/workspace.d.ts.map +1 -0
  247. package/dist/trpc/routers/workspace.js +203 -0
  248. package/dist/trpc/routers/workspace.js.map +1 -0
  249. package/dist/unified-websocket-runner.d.ts +280 -0
  250. package/dist/unified-websocket-runner.d.ts.map +1 -0
  251. package/dist/unified-websocket-runner.js +3840 -0
  252. package/dist/unified-websocket-runner.js.map +1 -0
  253. package/dist/workspace-api.d.ts +14 -0
  254. package/dist/workspace-api.d.ts.map +1 -0
  255. package/dist/workspace-api.js +104 -0
  256. package/dist/workspace-api.js.map +1 -0
  257. package/dist/ws-adapter.d.ts +25 -0
  258. package/dist/ws-adapter.d.ts.map +1 -0
  259. package/dist/ws-adapter.js +63 -0
  260. package/dist/ws-adapter.js.map +1 -0
  261. 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