@tuongaz/seeflow 0.1.42 → 0.1.51

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/src/mcp.ts CHANGED
@@ -12,26 +12,11 @@ import {
12
12
  CreateProjectBodySchema,
13
13
  NodePatchBodySchema,
14
14
  NodesBulkBodySchema,
15
- type OperationsDeps,
15
+ type Operations,
16
16
  PositionBodySchema,
17
17
  RegisterBodySchema,
18
18
  ReorderBodySchema,
19
- addConnectorImpl,
20
- addConnectorsBulkImpl,
21
- addNodeImpl,
22
- addNodesBulkImpl,
23
- createProjectImpl,
24
- deleteConnectorImpl,
25
- deleteFlowImpl,
26
- deleteNodeImpl,
27
- getFlowImpl,
28
- listDemosImpl,
29
- moveNodeImpl,
30
- patchConnectorImpl,
31
- patchNodeImpl,
32
- registerFlowImpl,
33
- reorderNodeImpl,
34
- validateImpl,
19
+ createOperations,
35
20
  } from './operations.ts';
36
21
  import type { Registry } from './registry.ts';
37
22
  import type { FlowWatcher } from './watcher.ts';
@@ -187,13 +172,25 @@ const DeleteConnectorInputSchema = z.object({
187
172
  connectorId: z.string().min(1),
188
173
  });
189
174
 
190
- const buildTools = (deps: OperationsDeps): McpTool[] => [
175
+ const buildTools = (ops: Operations): McpTool[] => [
191
176
  {
192
177
  name: 'seeflow_list_flows',
193
178
  description: 'List every flow registered with the studio.',
194
179
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
195
180
  handler: async () => {
196
- const result = listDemosImpl(deps);
181
+ const result = ops.listFlows();
182
+ return okResult(result.data);
183
+ },
184
+ },
185
+ {
186
+ name: 'seeflow_list_flows_summary',
187
+ description:
188
+ 'List registered flows as { id, name, description } only. Use this for ' +
189
+ 'cheap discovery before drilling into a specific flow with ' +
190
+ 'seeflow_get_flow_graph or seeflow_get_node.',
191
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
192
+ handler: async () => {
193
+ const result = ops.listFlowsSummary();
197
194
  return okResult(result.data);
198
195
  },
199
196
  },
@@ -217,7 +214,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
217
214
  if (!body || !('flow' in body)) {
218
215
  return errorResult('Body must include `flow`');
219
216
  }
220
- const result = validateImpl({
217
+ const result = ops.validate({
221
218
  flow: body.flow,
222
219
  style: body.style as unknown,
223
220
  });
@@ -231,7 +228,28 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
231
228
  handler: async (args) => {
232
229
  const v = requireFlowId(args);
233
230
  if ('error' in v) return errorResult(v.error);
234
- const result = await getFlowImpl(deps, v.flowId);
231
+ const result = await ops.getFlow(v.flowId);
232
+ switch (result.kind) {
233
+ case 'ok':
234
+ return okResult(result.data);
235
+ case 'notFound':
236
+ return errorResult('not found');
237
+ case 'fileNotFound':
238
+ return errorResult(`Flow file not found: ${result.path}`);
239
+ }
240
+ },
241
+ },
242
+ {
243
+ name: 'seeflow_get_flow_graph',
244
+ description:
245
+ "Get a flow's nodes + connectors without inlining per-node file-backed " +
246
+ 'content (`detail`, `html`). Cheap topology read — pair with seeflow_get_node ' +
247
+ "when you need a specific node's long-form body.",
248
+ inputSchema: FLOW_ID_INPUT_SCHEMA,
249
+ handler: async (args) => {
250
+ const v = requireFlowId(args);
251
+ if ('error' in v) return errorResult(v.error);
252
+ const result = await ops.getFlowGraph(v.flowId);
235
253
  switch (result.kind) {
236
254
  case 'ok':
237
255
  return okResult(result.data);
@@ -239,6 +257,56 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
239
257
  return errorResult('not found');
240
258
  case 'fileNotFound':
241
259
  return errorResult(`Flow file not found: ${result.path}`);
260
+ case 'badJson':
261
+ return errorResult(`Flow file is not valid JSON: ${result.detail}`);
262
+ case 'badSchema':
263
+ return errorResult(
264
+ `Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
265
+ );
266
+ }
267
+ },
268
+ },
269
+ {
270
+ name: 'seeflow_get_node',
271
+ description:
272
+ 'Get a single node from a flow with its file-backed content (detail.md, ' +
273
+ 'view.html) inlined. Use after seeflow_get_flow_graph to drill into one node.',
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: {
277
+ flowId: { type: 'string', minLength: 1 },
278
+ nodeId: { type: 'string', minLength: 1 },
279
+ },
280
+ required: ['flowId', 'nodeId'],
281
+ additionalProperties: false,
282
+ },
283
+ handler: async (args) => {
284
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
285
+ return errorResult('Invalid arguments: expected an object with flowId + nodeId');
286
+ }
287
+ const { flowId, nodeId } = args as { flowId?: unknown; nodeId?: unknown };
288
+ if (typeof flowId !== 'string' || flowId.length === 0) {
289
+ return errorResult('Invalid arguments: flowId must be a non-empty string');
290
+ }
291
+ if (typeof nodeId !== 'string' || nodeId.length === 0) {
292
+ return errorResult('Invalid arguments: nodeId must be a non-empty string');
293
+ }
294
+ const result = await ops.getNode(flowId, nodeId);
295
+ switch (result.kind) {
296
+ case 'ok':
297
+ return okResult(result.data);
298
+ case 'notFound':
299
+ return errorResult('not found');
300
+ case 'fileNotFound':
301
+ return errorResult(`Flow file not found: ${result.path}`);
302
+ case 'unknownNode':
303
+ return errorResult(`Unknown nodeId: ${nodeId}`);
304
+ case 'badJson':
305
+ return errorResult(`Flow file is not valid JSON: ${result.detail}`);
306
+ case 'badSchema':
307
+ return errorResult(
308
+ `Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
309
+ );
242
310
  }
243
311
  },
244
312
  },
@@ -251,7 +319,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
251
319
  if (!parsed.success) {
252
320
  return errorResult(`Invalid register body: ${JSON.stringify(parsed.error.issues)}`);
253
321
  }
254
- const result = await registerFlowImpl(deps, parsed.data);
322
+ const result = await ops.registerFlow(parsed.data);
255
323
  switch (result.kind) {
256
324
  case 'ok':
257
325
  return okResult(result.data);
@@ -275,7 +343,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
275
343
  handler: async (args) => {
276
344
  const v = requireFlowId(args);
277
345
  if ('error' in v) return errorResult(v.error);
278
- const result = deleteFlowImpl(deps, v.flowId);
346
+ const result = ops.deleteFlow(v.flowId);
279
347
  switch (result.kind) {
280
348
  case 'ok':
281
349
  return okResult({ ok: true });
@@ -293,7 +361,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
293
361
  if (!parsed.success) {
294
362
  return errorResult(`Invalid create project body: ${JSON.stringify(parsed.error.issues)}`);
295
363
  }
296
- const result = await createProjectImpl(deps, parsed.data);
364
+ const result = await ops.createProject(parsed.data);
297
365
  switch (result.kind) {
298
366
  case 'ok':
299
367
  return okResult(result.data);
@@ -321,7 +389,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
321
389
  return errorResult(`Invalid add_node arguments: ${JSON.stringify(parsed.error.issues)}`);
322
390
  }
323
391
  const { flowId, node } = parsed.data;
324
- const result = await addNodeImpl(deps, flowId, node);
392
+ const result = await ops.addNode(flowId, node);
325
393
  switch (result.kind) {
326
394
  case 'ok':
327
395
  return okResult({ ok: true, id: result.data.id, node: result.data.node });
@@ -349,7 +417,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
349
417
  return errorResult(`Invalid add_nodes arguments: ${JSON.stringify(parsed.error.issues)}`);
350
418
  }
351
419
  const { flowId, nodes } = parsed.data;
352
- const result = await addNodesBulkImpl(deps, flowId, { nodes });
420
+ const result = await ops.addNodesBulk(flowId, { nodes });
353
421
  switch (result.kind) {
354
422
  case 'ok':
355
423
  return okResult({ ok: true, nodes: result.data.nodes });
@@ -380,7 +448,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
380
448
  return errorResult(`Invalid delete_node arguments: ${JSON.stringify(parsed.error.issues)}`);
381
449
  }
382
450
  const { flowId, nodeId } = parsed.data;
383
- const result = await deleteNodeImpl(deps, flowId, nodeId);
451
+ const result = await ops.deleteNode(flowId, nodeId);
384
452
  switch (result.kind) {
385
453
  case 'ok':
386
454
  return okResult({ ok: true });
@@ -409,7 +477,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
409
477
  return errorResult(`Invalid move_node arguments: ${JSON.stringify(parsed.error.issues)}`);
410
478
  }
411
479
  const { flowId, nodeId, x, y } = parsed.data;
412
- const result = await moveNodeImpl(deps, flowId, nodeId, { x, y });
480
+ const result = await ops.moveNode(flowId, nodeId, { x, y });
413
481
  switch (result.kind) {
414
482
  case 'ok':
415
483
  return okResult({ ok: true, position: result.data.position });
@@ -439,7 +507,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
439
507
  return errorResult(`Invalid patch_node arguments: ${JSON.stringify(parsed.error.issues)}`);
440
508
  }
441
509
  const { flowId, nodeId, ...updates } = parsed.data;
442
- const result = await patchNodeImpl(deps, flowId, nodeId, updates);
510
+ const result = await ops.patchNode(flowId, nodeId, updates);
443
511
  switch (result.kind) {
444
512
  case 'ok':
445
513
  return okResult({ ok: true });
@@ -475,7 +543,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
475
543
  // reorderNodeImpl receives the same discriminated union the REST route
476
544
  // does — keeps a single source of truth for op semantics.
477
545
  const reorderBody = ReorderBodySchema.parse(body);
478
- const result = await reorderNodeImpl(deps, flowId, nodeId, reorderBody);
546
+ const result = await ops.reorderNode(flowId, nodeId, reorderBody);
479
547
  switch (result.kind) {
480
548
  case 'ok':
481
549
  return okResult({ ok: true });
@@ -507,7 +575,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
507
575
  );
508
576
  }
509
577
  const { flowId, connector } = parsed.data;
510
- const result = await addConnectorImpl(deps, flowId, connector);
578
+ const result = await ops.addConnector(flowId, connector);
511
579
  switch (result.kind) {
512
580
  case 'ok':
513
581
  return okResult({ ok: true, id: result.data.id });
@@ -537,7 +605,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
537
605
  );
538
606
  }
539
607
  const { flowId, connectors } = parsed.data;
540
- const result = await addConnectorsBulkImpl(deps, flowId, { connectors });
608
+ const result = await ops.addConnectorsBulk(flowId, { connectors });
541
609
  switch (result.kind) {
542
610
  case 'ok':
543
611
  return okResult({ ok: true, connectors: result.data.connectors });
@@ -571,7 +639,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
571
639
  );
572
640
  }
573
641
  const { flowId, connectorId, ...updates } = parsed.data;
574
- const result = await patchConnectorImpl(deps, flowId, connectorId, updates);
642
+ const result = await ops.patchConnector(flowId, connectorId, updates);
575
643
  switch (result.kind) {
576
644
  case 'ok':
577
645
  return okResult({ ok: true });
@@ -602,7 +670,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
602
670
  );
603
671
  }
604
672
  const { flowId, connectorId } = parsed.data;
605
- const result = await deleteConnectorImpl(deps, flowId, connectorId);
673
+ const result = await ops.deleteConnector(flowId, connectorId);
606
674
  switch (result.kind) {
607
675
  case 'ok':
608
676
  return okResult({ ok: true });
@@ -630,11 +698,12 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
630
698
  * mcp-shim.ts) — every request builds its own server in stateless mode.
631
699
  */
632
700
  export function createMcpServer(options: CreateMcpServerOptions): Server {
633
- const tools = buildTools({
701
+ const ops = createOperations({
634
702
  registry: options.registry,
635
703
  watcher: options.watcher,
636
704
  projectBaseDir: options.projectBaseDir,
637
705
  });
706
+ const tools = buildTools(ops);
638
707
 
639
708
  const server = new Server({ name: 'seeflow', version: '0.1.0' }, { capabilities: { tools: {} } });
640
709
 
package/src/merge.ts CHANGED
@@ -31,6 +31,7 @@ export function mergeFlowAndStyle(flow: Flow, style: Style): ResolvedFlow {
31
31
  return {
32
32
  version: flow.version,
33
33
  name: flow.name,
34
+ ...(flow.description !== undefined ? { description: flow.description } : {}),
34
35
  ...(flow.resetAction ? { resetAction: flow.resetAction } : {}),
35
36
  nodes: mergedNodes,
36
37
  connectors: mergedConnectors,
@@ -111,6 +112,7 @@ const CONNECTOR_STYLE_KEYS = new Set([
111
112
  export function splitFlow(resolved: {
112
113
  version: number;
113
114
  name: string;
115
+ description?: string;
114
116
  resetAction?: unknown;
115
117
  nodes: Array<Record<string, unknown>>;
116
118
  connectors: Array<Record<string, unknown>>;
@@ -179,6 +181,7 @@ export function splitFlow(resolved: {
179
181
  nodes: flowNodes,
180
182
  connectors: flowConnectors,
181
183
  };
184
+ if (resolved.description !== undefined) flow.description = resolved.description;
182
185
  if (resolved.resetAction !== undefined) flow.resetAction = resolved.resetAction;
183
186
 
184
187
  const style: Record<string, unknown> = {};
package/src/node-files.ts CHANGED
@@ -29,8 +29,11 @@ export type ExternalizedFieldName = (typeof EXTERNALIZED_NODE_FIELDS)[number]['f
29
29
  export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
30
30
  `nodes/${nodeId}/${fileName}`;
31
31
 
32
- export const nodeFileRef = (nodeId: string, fileName: string): string =>
33
- `file://${nodeFileRelPath(nodeId, fileName)}`;
32
+ // Node-relative ref: the resolver knows the enclosing node id from the flow.json
33
+ // shape (nodes[i].id), so the on-disk string only needs the filename. Kept as a
34
+ // 2-arg helper so call sites don't change shape and the spec stays explicit
35
+ // that the file lives under the given node.
36
+ export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
34
37
 
35
38
  export const nodeFileAbsPath = (repoPath: string, nodeId: string, fileName: string): string =>
36
39
  join(repoPath, '.seeflow', nodeFileRelPath(nodeId, fileName));