@tuongaz/seeflow 0.1.55 → 0.1.57

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/api.ts CHANGED
@@ -15,10 +15,9 @@ import type { EventBus } from './events.ts';
15
15
  import { type LayoutOptions, computeLayout } from './layout.ts';
16
16
  import {
17
17
  ConnectorPatchBodySchema,
18
- ConnectorsBulkBodySchema,
19
18
  CreateProjectBodySchema,
19
+ FlowBulkBodySchema,
20
20
  NodePatchBodySchema,
21
- NodesBulkBodySchema,
22
21
  PositionBodySchema,
23
22
  RegisterBodySchema,
24
23
  ReorderBodySchema,
@@ -1053,11 +1052,14 @@ export function createApi(options: ApiOptions): Hono {
1053
1052
  }
1054
1053
  });
1055
1054
 
1056
- // Bulk-create up to 100 nodes in one transactional write. Either the whole
1057
- // batch lands and a single flow:reload broadcast fires, or nothing lands.
1058
- // Intended for skill/LLM seeding where N singular calls would burn tokens
1059
- // and round-trip latency. Per-item shape mirrors the singular endpoint.
1060
- api.post('/flows/:id/nodes/bulk', async (c) => {
1055
+ // Bulk-create up to 100 nodes + 100 connectors in one transactional write.
1056
+ // Either the whole batch lands (single flow:reload broadcast) or nothing
1057
+ // does a post-mutation ResolvedFlowSchema reject (e.g. dangling connector
1058
+ // source/target) rolls back both arrays together. Connectors may reference
1059
+ // nodes added in the same call; the parse sees the merged graph as a whole.
1060
+ // Intended for skill/LLM seeding where multiple singular calls would burn
1061
+ // tokens and round-trip latency.
1062
+ api.post('/flows/:id/bulk', async (c) => {
1061
1063
  const id = c.req.param('id');
1062
1064
 
1063
1065
  let body: unknown;
@@ -1066,15 +1068,19 @@ export function createApi(options: ApiOptions): Hono {
1066
1068
  } catch {
1067
1069
  return c.json({ error: 'Body must be valid JSON' }, 400);
1068
1070
  }
1069
- const parsed = NodesBulkBodySchema.safeParse(body);
1071
+ const parsed = FlowBulkBodySchema.safeParse(body);
1070
1072
  if (!parsed.success) {
1071
- return c.json({ error: 'Invalid bulk nodes body', issues: parsed.error.issues }, 400);
1073
+ return c.json({ error: 'Invalid bulk body', issues: parsed.error.issues }, 400);
1072
1074
  }
1073
1075
 
1074
- const result = await ops.addNodesBulk(id, parsed.data);
1076
+ const result = await ops.addBulk(id, parsed.data);
1075
1077
  switch (result.kind) {
1076
1078
  case 'ok':
1077
- return c.json({ ok: true, nodes: result.data.nodes });
1079
+ return c.json({
1080
+ ok: true,
1081
+ nodes: result.data.nodes,
1082
+ connectors: result.data.connectors,
1083
+ });
1078
1084
  case 'flowNotFound':
1079
1085
  return c.json({ error: 'unknown demo' }, 404);
1080
1086
  case 'fileNotFound':
@@ -1084,9 +1090,14 @@ export function createApi(options: ApiOptions): Hono {
1084
1090
  case 'badSchema':
1085
1091
  return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
1086
1092
  case 'duplicateIdInBatch':
1087
- return c.json({ error: `Duplicate id in batch: ${result.id}` }, 400);
1093
+ return c.json({ error: `Duplicate ${result.collection} id in batch: ${result.id}` }, 400);
1088
1094
  case 'idAlreadyExists':
1089
- return c.json({ error: `Node id already exists: ${result.id}` }, 400);
1095
+ return c.json(
1096
+ {
1097
+ error: `${result.collection === 'nodes' ? 'Node' : 'Connector'} id already exists: ${result.id}`,
1098
+ },
1099
+ 400,
1100
+ );
1090
1101
  case 'writeFailed':
1091
1102
  return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
1092
1103
  }
@@ -1195,44 +1206,6 @@ export function createApi(options: ApiOptions): Hono {
1195
1206
  }
1196
1207
  });
1197
1208
 
1198
- // Bulk-create up to 100 connectors in one transactional write. Mirrors the
1199
- // /nodes/bulk shape. Dangling source/target on any item rolls back the whole
1200
- // batch via the post-mutation ResolvedFlowSchema parse.
1201
- api.post('/flows/:id/connectors/bulk', async (c) => {
1202
- const id = c.req.param('id');
1203
-
1204
- let body: unknown;
1205
- try {
1206
- body = await c.req.json();
1207
- } catch {
1208
- return c.json({ error: 'Body must be valid JSON' }, 400);
1209
- }
1210
- const parsed = ConnectorsBulkBodySchema.safeParse(body);
1211
- if (!parsed.success) {
1212
- return c.json({ error: 'Invalid bulk connectors body', issues: parsed.error.issues }, 400);
1213
- }
1214
-
1215
- const result = await ops.addConnectorsBulk(id, parsed.data);
1216
- switch (result.kind) {
1217
- case 'ok':
1218
- return c.json({ ok: true, connectors: result.data.connectors });
1219
- case 'flowNotFound':
1220
- return c.json({ error: 'unknown demo' }, 404);
1221
- case 'fileNotFound':
1222
- return c.json({ error: `Flow file not found: ${result.path}` }, 404);
1223
- case 'badJson':
1224
- return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
1225
- case 'badSchema':
1226
- return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
1227
- case 'duplicateIdInBatch':
1228
- return c.json({ error: `Duplicate id in batch: ${result.id}` }, 400);
1229
- case 'idAlreadyExists':
1230
- return c.json({ error: `Connector id already exists: ${result.id}` }, 400);
1231
- case 'writeFailed':
1232
- return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
1233
- }
1234
- });
1235
-
1236
1209
  // DELETE a connector. Just removes the entry from demo.connectors — node
1237
1210
  // deletion is what cascades, not connector deletion.
1238
1211
  api.delete('/flows/:id/connectors/:connId', async (c) => {
@@ -101,10 +101,26 @@ function describeOutcome(outcome: { kind: string } & Record<string, unknown>): s
101
101
  return `Flow file is not valid JSON: ${String(outcome.detail ?? outcome.message ?? '')}`;
102
102
  case 'badSchema':
103
103
  return `Flow failed schema validation: ${JSON.stringify(outcome.issues ?? [])}`;
104
- case 'duplicateIdInBatch':
105
- return `Duplicate id in batch: ${String(outcome.id ?? '')}`;
106
- case 'idAlreadyExists':
107
- return `Id already exists: ${String(outcome.id ?? '')}`;
104
+ case 'duplicateIdInBatch': {
105
+ // `collection` is present on the FlowBulk outcome ('nodes' | 'connectors')
106
+ // and absent on legacy singular outcomes — keep both shapes working.
107
+ const collection = outcome.collection;
108
+ const prefix =
109
+ collection === 'nodes' || collection === 'connectors'
110
+ ? `Duplicate ${collection} id in batch`
111
+ : 'Duplicate id in batch';
112
+ return `${prefix}: ${String(outcome.id ?? '')}`;
113
+ }
114
+ case 'idAlreadyExists': {
115
+ const collection = outcome.collection;
116
+ const prefix =
117
+ collection === 'nodes'
118
+ ? 'Node id already exists'
119
+ : collection === 'connectors'
120
+ ? 'Connector id already exists'
121
+ : 'Id already exists';
122
+ return `${prefix}: ${String(outcome.id ?? '')}`;
123
+ }
108
124
  case 'writeFailed':
109
125
  return `Failed to write demo file: ${String(outcome.message ?? '')}`;
110
126
  case 'sdkWriteFailed':
@@ -6,10 +6,9 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
6
6
  import { EXIT_CODE_BY_KIND, exitCodeForKind } from './cli-helpers.ts';
7
7
  import {
8
8
  ConnectorPatchBodySchema,
9
- ConnectorsBulkBodySchema,
10
9
  CreateProjectBodySchema,
10
+ FlowBulkBodySchema,
11
11
  NodePatchBodySchema,
12
- NodesBulkBodySchema,
13
12
  PositionBodySchema,
14
13
  RegisterBodySchema,
15
14
  ReorderBodySchema,
@@ -252,6 +251,40 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
252
251
  requiresStudio: false,
253
252
  examples: ['seeflow flows:layout abc12345'],
254
253
  },
254
+ {
255
+ name: 'flow:add-bulk',
256
+ synopsis: 'seeflow flow:add-bulk <flowId> [--json | --file | --stdin]',
257
+ description:
258
+ 'Add up to 100 nodes + 100 connectors atomically. Body shape: ' +
259
+ '`{ nodes?: Node[], connectors?: Connector[] }` (at least one non-empty). ' +
260
+ 'Connectors may reference nodes added in the same batch; the whole flow ' +
261
+ 'is re-validated post-merge so a dangling source/target — or any per-item ' +
262
+ 'schema failure — rolls back both arrays together and emits no broadcast.',
263
+ category: 'flows',
264
+ args: [{ name: 'flowId', required: true, description: 'Flow id or slug' }],
265
+ flags: BODY_FLAGS,
266
+ body: { schemaRef: 'FlowBulkBody' },
267
+ outputs: {
268
+ okExample: {
269
+ nodes: [{ id: 'node-a', node: { id: 'node-a' } }],
270
+ connectors: [{ id: 'conn-a' }],
271
+ },
272
+ errorKinds: [
273
+ 'flowNotFound',
274
+ 'fileNotFound',
275
+ 'badJson',
276
+ 'badSchema',
277
+ 'duplicateIdInBatch',
278
+ 'idAlreadyExists',
279
+ 'writeFailed',
280
+ ],
281
+ },
282
+ requiresStudio: false,
283
+ examples: [
284
+ 'seeflow flow:add-bulk abc12345 --json \'{"nodes":[{"id":"a","type":"shapeNode","data":{"shape":"rectangle"}}],"connectors":[]}\'',
285
+ 'seeflow flow:add-bulk abc12345 --file batch.json',
286
+ ],
287
+ },
255
288
  {
256
289
  name: 'flows:play',
257
290
  synopsis: 'seeflow flows:play <flowId> <nodeId>',
@@ -317,25 +350,6 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
317
350
  'seeflow nodes:add abc12345 --json \'{"type":"shapeNode","data":{"shape":"rectangle"}}\'',
318
351
  ],
319
352
  },
320
- {
321
- name: 'nodes:add-bulk',
322
- synopsis: 'seeflow nodes:add-bulk <flowId> [--json | --file | --stdin]',
323
- description:
324
- 'Add up to 100 nodes in one transactional write. Body shape: ' +
325
- '`{ nodes: Node[] }`. Any duplicate id rolls back the whole batch.',
326
- category: 'nodes',
327
- args: [{ name: 'flowId', required: true, description: 'Flow id or slug' }],
328
- flags: BODY_FLAGS,
329
- body: { schemaRef: 'NodesBulkBody' },
330
- outputs: {
331
- okExample: { ids: ['a', 'b'] },
332
- errorKinds: ['flowNotFound', 'fileNotFound', 'badSchema', 'duplicateIdInBatch'],
333
- },
334
- requiresStudio: false,
335
- examples: [
336
- 'seeflow nodes:add-bulk abc12345 --json \'{"nodes":[{"id":"a","type":"shapeNode","data":{"shape":"rectangle"}}]}\'',
337
- ],
338
- },
339
353
  {
340
354
  name: 'nodes:get',
341
355
  synopsis: 'seeflow nodes:get <flowId> <nodeId>',
@@ -451,18 +465,6 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
451
465
  'seeflow connectors:add abc12345 --json \'{"source":{"nodeId":"a"},"target":{"nodeId":"b"}}\'',
452
466
  ],
453
467
  },
454
- {
455
- name: 'connectors:add-bulk',
456
- synopsis: 'seeflow connectors:add-bulk <flowId> [--json | --file | --stdin]',
457
- description: 'Add up to 100 connectors transactionally. Body: `{ connectors: Connector[] }`.',
458
- category: 'connectors',
459
- args: [{ name: 'flowId', required: true, description: 'Flow id or slug' }],
460
- flags: BODY_FLAGS,
461
- body: { schemaRef: 'ConnectorsBulkBody' },
462
- outputs: { errorKinds: ['flowNotFound', 'badSchema', 'duplicateIdInBatch'] },
463
- requiresStudio: false,
464
- examples: ['seeflow connectors:add-bulk abc12345 --file connectors.json'],
465
- },
466
468
  {
467
469
  name: 'connectors:patch',
468
470
  synopsis: 'seeflow connectors:patch <flowId> <connectorId> [--json | --file | --stdin]',
@@ -542,10 +544,8 @@ function resolveSchemaRef(ref: string): unknown {
542
544
  return zodToJsonSchema(NodePatchBodySchema, { $refStrategy: 'none' });
543
545
  case 'ConnectorPatchBody':
544
546
  return zodToJsonSchema(ConnectorPatchBodySchema, { $refStrategy: 'none' });
545
- case 'NodesBulkBody':
546
- return zodToJsonSchema(NodesBulkBodySchema, { $refStrategy: 'none' });
547
- case 'ConnectorsBulkBody':
548
- return zodToJsonSchema(ConnectorsBulkBodySchema, { $refStrategy: 'none' });
547
+ case 'FlowBulkBody':
548
+ return zodToJsonSchema(FlowBulkBodySchema, { $refStrategy: 'none' });
549
549
  case 'CreateProjectBody':
550
550
  return zodToJsonSchema(CreateProjectBodySchema, { $refStrategy: 'none' });
551
551
  case 'RegisterBody':
package/src/cli.ts CHANGED
@@ -8,9 +8,8 @@ import { createEventBus } from './events.ts';
8
8
  import type { LayoutOptions } from './layout.ts';
9
9
  import {
10
10
  ConnectorPatchBodySchema,
11
- ConnectorsBulkBodySchema,
11
+ FlowBulkBodySchema,
12
12
  NodePatchBodySchema,
13
- NodesBulkBodySchema,
14
13
  ReorderBodySchema,
15
14
  } from './operations.ts';
16
15
  import { seeflowHome } from './paths.ts';
@@ -134,12 +133,12 @@ if (argv.includes('--version') || argv.includes('-v')) {
134
133
  await runFlowsDelete();
135
134
  } else if (sub === 'flows:layout') {
136
135
  await runFlowsLayout();
136
+ } else if (sub === 'flow:add-bulk') {
137
+ await runFlowAddBulk();
137
138
  } else if (sub === 'flows:play') {
138
139
  await runFlowsPlay();
139
140
  } else if (sub === 'nodes:add') {
140
141
  await runNodesAdd();
141
- } else if (sub === 'nodes:add-bulk') {
142
- await runNodesAddBulk();
143
142
  } else if (sub === 'nodes:get') {
144
143
  await runNodesGet();
145
144
  } else if (sub === 'nodes:patch') {
@@ -152,8 +151,6 @@ if (argv.includes('--version') || argv.includes('-v')) {
152
151
  await runNodesDelete();
153
152
  } else if (sub === 'connectors:add') {
154
153
  await runConnectorsAdd();
155
- } else if (sub === 'connectors:add-bulk') {
156
- await runConnectorsAddBulk();
157
154
  } else if (sub === 'connectors:patch') {
158
155
  await runConnectorsPatch();
159
156
  } else if (sub === 'connectors:delete') {
@@ -188,15 +185,14 @@ Commands (work without a running studio):
188
185
  flows:graph <id> List nodes + connectors without inlined file content
189
186
  flows:delete <id> Unregister a flow
190
187
  flows:layout <id> Apply ELK layout, writing style.json (--json/--file/--stdin optional)
188
+ flow:add-bulk <id> Add many nodes + connectors atomically (--json/--file/--stdin; body { nodes?, connectors? })
191
189
  nodes:add <id> Add a node (--json/--file/--stdin)
192
- nodes:add-bulk <id> Add many nodes (--json/--file/--stdin)
193
190
  nodes:get <id> <n> Get a node with detail / html content inlined
194
191
  nodes:patch <id> <n> Patch a node (--json/--file/--stdin)
195
192
  nodes:move <id> <n> Move a node (--x N --y N)
196
193
  nodes:reorder <id> <n> Reorder a node (--op forward|backward|toFront|toBack|toIndex [--index N])
197
194
  nodes:delete <id> <n> Delete a node
198
195
  connectors:add <id> Add a connector (--json/--file/--stdin)
199
- connectors:add-bulk <id> Add many connectors (--json/--file/--stdin)
200
196
  connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
201
197
  connectors:delete <id> <connId> Delete a connector
202
198
  validate Schema-validate a flow.json (--file <file> [--style <file>])
@@ -661,15 +657,15 @@ async function runNodesAdd() {
661
657
  printOutcome(result);
662
658
  }
663
659
 
664
- async function runNodesAddBulk() {
660
+ async function runFlowAddBulk() {
665
661
  const flowId = requireArg(1, '<flowId>');
666
662
  const body = await bodyFromFlags();
667
- const parsed = NodesBulkBodySchema.safeParse(body);
663
+ const parsed = FlowBulkBodySchema.safeParse(body);
668
664
  if (!parsed.success) {
669
- printError(`Invalid nodes:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
665
+ printError(`Invalid flow:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
670
666
  }
671
667
  const ops = createCliOperations();
672
- const result = await ops.addNodesBulk(flowId, parsed.data);
668
+ const result = await ops.addBulk(flowId, parsed.data);
673
669
  printOutcome(result);
674
670
  }
675
671
 
@@ -755,18 +751,6 @@ async function runConnectorsAdd() {
755
751
  printOutcome(result);
756
752
  }
757
753
 
758
- async function runConnectorsAddBulk() {
759
- const flowId = requireArg(1, '<flowId>');
760
- const body = await bodyFromFlags();
761
- const parsed = ConnectorsBulkBodySchema.safeParse(body);
762
- if (!parsed.success) {
763
- printError(`Invalid connectors:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
764
- }
765
- const ops = createCliOperations();
766
- const result = await ops.addConnectorsBulk(flowId, parsed.data);
767
- printOutcome(result);
768
- }
769
-
770
754
  async function runConnectorsPatch() {
771
755
  const flowId = requireArg(1, '<flowId>');
772
756
  const connId = requireArg(2, '<connectorId>');
package/src/mcp.ts CHANGED
@@ -8,15 +8,16 @@ import { type ZodTypeAny, z } from 'zod';
8
8
  import { zodToJsonSchema } from 'zod-to-json-schema';
9
9
  import {
10
10
  ConnectorPatchBodySchema,
11
- ConnectorsBulkBodySchema,
12
11
  CreateProjectBodySchema,
12
+ FLOW_BULK_NON_EMPTY_MESSAGE,
13
+ FlowBulkBodyShape,
13
14
  NodePatchBodySchema,
14
- NodesBulkBodySchema,
15
15
  type Operations,
16
16
  PositionBodySchema,
17
17
  RegisterBodySchema,
18
18
  ReorderBodySchema,
19
19
  createOperations,
20
+ flowBulkNonEmpty,
20
21
  } from './operations.ts';
21
22
  import type { Registry } from './registry.ts';
22
23
  import type { FlowWatcher } from './watcher.ts';
@@ -103,13 +104,16 @@ const AddNodeInputSchema = z.object({
103
104
  node: z.record(z.unknown()),
104
105
  });
105
106
 
106
- // add_nodes input: { flowId, nodes: [...] }. Same loose per-item shape as
107
- // add_node — ResolvedFlowSchema runs once over the whole batch server-side
108
- // after the merge. Min/max bounds come from NodesBulkBodySchema so the
109
- // 100-item cap shows up in the JSON Schema the agent introspects.
110
- const AddNodesInputSchema = NodesBulkBodySchema.extend({
107
+ // add_bulk input: { flowId, nodes?: [...], connectors?: [...] }. Same loose
108
+ // per-item shape as add_node / add_connector — ResolvedFlowSchema runs once
109
+ // over the whole merged graph server-side after the batch lands. The
110
+ // 100-per-kind cap and "at least one non-empty" invariant come from
111
+ // FlowBulkBodyShape + flowBulkNonEmpty (the unrefined object + reusable
112
+ // predicate exported by operations.ts) so the JSON Schema the agent
113
+ // introspects stays a clean object — not an intersection.
114
+ const AddBulkInputSchema = FlowBulkBodyShape.extend({
111
115
  flowId: z.string().min(1),
112
- });
116
+ }).refine(flowBulkNonEmpty, { message: FLOW_BULK_NON_EMPTY_MESSAGE });
113
117
 
114
118
  const DeleteNodeInputSchema = FlowNodeIdBaseSchema;
115
119
 
@@ -153,11 +157,6 @@ const AddConnectorInputSchema = z.object({
153
157
  connector: z.record(z.unknown()),
154
158
  });
155
159
 
156
- // add_connectors input: { flowId, connectors: [...] }. Mirrors add_nodes.
157
- const AddConnectorsInputSchema = ConnectorsBulkBodySchema.extend({
158
- flowId: z.string().min(1),
159
- });
160
-
161
160
  // patch_connector input: { flowId, connectorId } merged with the strict
162
161
  // ConnectorPatchBodySchema. .extend() preserves strict mode so unknown
163
162
  // top-level keys trip the Zod parse before any IO — matching the REST
@@ -407,20 +406,24 @@ const buildTools = (ops: Operations): McpTool[] => [
407
406
  },
408
407
  },
409
408
  {
410
- name: 'seeflow_add_nodes',
409
+ name: 'seeflow_add_bulk',
411
410
  description:
412
- 'Append 1–100 nodes to a flow in a single transactional write. Either every node lands or nothing does — if any item fails schema validation the whole batch is rejected. Use this instead of multiple seeflow_add_node calls when seeding a flow; it avoids per-item round-trips. Same per-item shape and externalization rules as seeflow_add_node.',
413
- inputSchema: inputSchemaFromZod(AddNodesInputSchema),
411
+ 'Append 1–100 nodes and 1–100 connectors to a flow in a SINGLE transactional write. Either every item lands or nothing does — a dangling connector source/target, a duplicate id, or any per-item schema failure rolls back BOTH arrays together (no flow:reload broadcast emitted). Connectors may reference nodes added in the same call. Body: { flowId, nodes?, connectors? } with at least one non-empty. Use this not multiple seeflow_add_node / seeflow_add_connector round-trips — when seeding a flow. Same per-item shape and externalization rules as the singular tools.',
412
+ inputSchema: inputSchemaFromZod(AddBulkInputSchema),
414
413
  handler: async (args) => {
415
- const parsed = AddNodesInputSchema.safeParse(args);
414
+ const parsed = AddBulkInputSchema.safeParse(args);
416
415
  if (!parsed.success) {
417
- return errorResult(`Invalid add_nodes arguments: ${JSON.stringify(parsed.error.issues)}`);
416
+ return errorResult(`Invalid add_bulk arguments: ${JSON.stringify(parsed.error.issues)}`);
418
417
  }
419
- const { flowId, nodes } = parsed.data;
420
- const result = await ops.addNodesBulk(flowId, { nodes });
418
+ const { flowId, nodes, connectors } = parsed.data;
419
+ const result = await ops.addBulk(flowId, { nodes, connectors });
421
420
  switch (result.kind) {
422
421
  case 'ok':
423
- return okResult({ ok: true, nodes: result.data.nodes });
422
+ return okResult({
423
+ ok: true,
424
+ nodes: result.data.nodes,
425
+ connectors: result.data.connectors,
426
+ });
424
427
  case 'flowNotFound':
425
428
  return errorResult('unknown demo');
426
429
  case 'fileNotFound':
@@ -430,9 +433,11 @@ const buildTools = (ops: Operations): McpTool[] => [
430
433
  case 'badSchema':
431
434
  return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
432
435
  case 'duplicateIdInBatch':
433
- return errorResult(`Duplicate id in batch: ${result.id}`);
436
+ return errorResult(`Duplicate ${result.collection} id in batch: ${result.id}`);
434
437
  case 'idAlreadyExists':
435
- return errorResult(`Node id already exists: ${result.id}`);
438
+ return errorResult(
439
+ `${result.collection === 'nodes' ? 'Node' : 'Connector'} id already exists: ${result.id}`,
440
+ );
436
441
  case 'writeFailed':
437
442
  return errorResult(`Failed to write demo file: ${result.message}`);
438
443
  }
@@ -592,40 +597,6 @@ const buildTools = (ops: Operations): McpTool[] => [
592
597
  }
593
598
  },
594
599
  },
595
- {
596
- name: 'seeflow_add_connectors',
597
- description:
598
- 'Append 1–100 connectors to a flow in a single transactional write. Either every connector lands or nothing does — a dangling source/target on any item rolls back the whole batch. Use after seeflow_add_nodes when seeding a flow.',
599
- inputSchema: inputSchemaFromZod(AddConnectorsInputSchema),
600
- handler: async (args) => {
601
- const parsed = AddConnectorsInputSchema.safeParse(args);
602
- if (!parsed.success) {
603
- return errorResult(
604
- `Invalid add_connectors arguments: ${JSON.stringify(parsed.error.issues)}`,
605
- );
606
- }
607
- const { flowId, connectors } = parsed.data;
608
- const result = await ops.addConnectorsBulk(flowId, { connectors });
609
- switch (result.kind) {
610
- case 'ok':
611
- return okResult({ ok: true, connectors: result.data.connectors });
612
- case 'flowNotFound':
613
- return errorResult('unknown demo');
614
- case 'fileNotFound':
615
- return errorResult(`Flow file not found: ${result.path}`);
616
- case 'badJson':
617
- return errorResult(`Flow file is not valid JSON: ${result.message}`);
618
- case 'badSchema':
619
- return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
620
- case 'duplicateIdInBatch':
621
- return errorResult(`Duplicate id in batch: ${result.id}`);
622
- case 'idAlreadyExists':
623
- return errorResult(`Connector id already exists: ${result.id}`);
624
- case 'writeFailed':
625
- return errorResult(`Failed to write demo file: ${result.message}`);
626
- }
627
- },
628
- },
629
600
  {
630
601
  name: 'seeflow_patch_connector',
631
602
  description: