@tuongaz/seeflow 0.1.3
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/README.md +95 -0
- package/bin/seeflow +32 -0
- package/bin/seeflow-mcp +23 -0
- package/dist/web/assets/html2canvas.esm-CBrSDip1.js +22 -0
- package/dist/web/assets/index-BlhIMoXf.js +8005 -0
- package/dist/web/assets/index-CIpouxGY.css +1 -0
- package/dist/web/assets/index.es-D6Hswegt.js +18 -0
- package/dist/web/assets/purify.es-CLGrRn1w.js +3 -0
- package/dist/web/index.html +13 -0
- package/examples/ecommerce-platform/.seeflow/scripts/play.ts +2 -0
- package/examples/ecommerce-platform/.seeflow/seeflow.json +250 -0
- package/examples/order-pipeline/.seeflow/scripts/play.ts +18 -0
- package/examples/order-pipeline/.seeflow/seeflow.json +86 -0
- package/examples/order-pipeline/README.md +11 -0
- package/examples/order-pipeline/package.json +6 -0
- package/package.json +55 -0
- package/public/runtime/tailwind.js +24394 -0
- package/src/api.ts +1093 -0
- package/src/cli.ts +329 -0
- package/src/demo.ts +65 -0
- package/src/diagram.ts +432 -0
- package/src/events.ts +70 -0
- package/src/mcp-shim.ts +93 -0
- package/src/mcp.ts +540 -0
- package/src/operations.ts +1192 -0
- package/src/process-spawner.ts +75 -0
- package/src/proxy.ts +393 -0
- package/src/registry.ts +139 -0
- package/src/runtime.ts +78 -0
- package/src/schema.ts +441 -0
- package/src/sdk-template.ts +37 -0
- package/src/sdk-writer.ts +37 -0
- package/src/server.ts +211 -0
- package/src/shellout.ts +30 -0
- package/src/status-runner.ts +374 -0
- package/src/watcher.ts +383 -0
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
type CallToolResult,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { type ZodTypeAny, z } from 'zod';
|
|
8
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
9
|
+
import {
|
|
10
|
+
ConnectorPatchBodySchema,
|
|
11
|
+
CreateProjectBodySchema,
|
|
12
|
+
NodePatchBodySchema,
|
|
13
|
+
type OperationsDeps,
|
|
14
|
+
PositionBodySchema,
|
|
15
|
+
RegisterBodySchema,
|
|
16
|
+
ReorderBodySchema,
|
|
17
|
+
addConnectorImpl,
|
|
18
|
+
addNodeImpl,
|
|
19
|
+
createProjectImpl,
|
|
20
|
+
deleteConnectorImpl,
|
|
21
|
+
deleteDemoImpl,
|
|
22
|
+
deleteNodeImpl,
|
|
23
|
+
getDemoImpl,
|
|
24
|
+
listDemosImpl,
|
|
25
|
+
moveNodeImpl,
|
|
26
|
+
patchConnectorImpl,
|
|
27
|
+
patchNodeImpl,
|
|
28
|
+
registerDemoImpl,
|
|
29
|
+
reorderNodeImpl,
|
|
30
|
+
} from './operations.ts';
|
|
31
|
+
import type { Registry } from './registry.ts';
|
|
32
|
+
import type { DemoWatcher } from './watcher.ts';
|
|
33
|
+
|
|
34
|
+
export interface CreateMcpServerOptions {
|
|
35
|
+
registry: Registry;
|
|
36
|
+
watcher?: DemoWatcher;
|
|
37
|
+
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
38
|
+
projectBaseDir?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Tools are pushed into this in-memory list inside `createMcpServer`. Each
|
|
42
|
+
// tool has a tiny one-sentence description, a JSON Schema for its input
|
|
43
|
+
// (built from existing Zod schemas via zod-to-json-schema where reuse is
|
|
44
|
+
// possible), and a handler that calls the same Outcome-returning inner
|
|
45
|
+
// helper the REST handler uses.
|
|
46
|
+
export interface McpTool {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
inputSchema: Record<string, unknown>;
|
|
50
|
+
handler: (args: unknown) => Promise<CallToolResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// zod-to-json-schema emits `$schema` and other top-level Draft fields by
|
|
54
|
+
// default. The MCP `tools/list` response carries `inputSchema` inline, so
|
|
55
|
+
// stripping the wrapper keeps the wire payload tidy without losing any of
|
|
56
|
+
// the actual shape constraints.
|
|
57
|
+
const inputSchemaFromZod = (schema: ZodTypeAny): Record<string, unknown> => {
|
|
58
|
+
const json = zodToJsonSchema(schema, { $refStrategy: 'none' }) as Record<string, unknown>;
|
|
59
|
+
const { $schema: _$schema, ...rest } = json;
|
|
60
|
+
return rest;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const okResult = (value: unknown): CallToolResult => ({
|
|
64
|
+
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const errorResult = (text: string): CallToolResult => ({
|
|
68
|
+
isError: true,
|
|
69
|
+
content: [{ type: 'text', text }],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Most MCP tools take a single demoId argument. Defined inline as plain
|
|
73
|
+
// JSON Schema (rather than a one-off Zod schema) because there's no REST
|
|
74
|
+
// counterpart to share with.
|
|
75
|
+
const DEMO_ID_INPUT_SCHEMA = {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: { demoId: { type: 'string', minLength: 1 } },
|
|
78
|
+
required: ['demoId'],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
const requireDemoId = (args: unknown): { demoId: string } | { error: string } => {
|
|
83
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
|
+
return { error: 'Invalid arguments: expected an object with demoId' };
|
|
85
|
+
}
|
|
86
|
+
const { demoId } = args as { demoId?: unknown };
|
|
87
|
+
if (typeof demoId !== 'string' || demoId.length === 0) {
|
|
88
|
+
return { error: 'Invalid arguments: demoId must be a non-empty string' };
|
|
89
|
+
}
|
|
90
|
+
return { demoId };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// {demoId, nodeId} body shape shared by move + reorder + delete inputs.
|
|
94
|
+
const DemoNodeIdBaseSchema = z.object({
|
|
95
|
+
demoId: z.string().min(1),
|
|
96
|
+
nodeId: z.string().min(1),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// add_node input: { demoId, node: <node payload> }. The inner `node` object is
|
|
100
|
+
// loose here (additionalProperties=true via passthrough) because DemoSchema
|
|
101
|
+
// runs the full validation server-side after the new node is merged in.
|
|
102
|
+
const AddNodeInputSchema = z.object({
|
|
103
|
+
demoId: z.string().min(1),
|
|
104
|
+
node: z.record(z.unknown()),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const DeleteNodeInputSchema = DemoNodeIdBaseSchema;
|
|
108
|
+
|
|
109
|
+
// move_node input: { demoId, nodeId } extended with PositionBodySchema's
|
|
110
|
+
// { x, y } fields so agents see one flat schema.
|
|
111
|
+
const MoveNodeInputSchema = DemoNodeIdBaseSchema.extend({
|
|
112
|
+
x: PositionBodySchema.shape.x,
|
|
113
|
+
y: PositionBodySchema.shape.y,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// reorder_node input: each branch of the existing ReorderBodySchema
|
|
117
|
+
// discriminated union extended with demoId/nodeId. Keeps the discriminator
|
|
118
|
+
// on `op` so the emitted JSON Schema is an oneOf the agent can introspect.
|
|
119
|
+
const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
120
|
+
DemoNodeIdBaseSchema.extend({ op: z.literal('forward') }),
|
|
121
|
+
DemoNodeIdBaseSchema.extend({ op: z.literal('backward') }),
|
|
122
|
+
DemoNodeIdBaseSchema.extend({ op: z.literal('toFront') }),
|
|
123
|
+
DemoNodeIdBaseSchema.extend({ op: z.literal('toBack') }),
|
|
124
|
+
DemoNodeIdBaseSchema.extend({
|
|
125
|
+
op: z.literal('toIndex'),
|
|
126
|
+
index: z.number().int().nonnegative(),
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// patch_node input: { demoId, nodeId } merged with NodePatchBodySchema's
|
|
131
|
+
// optional fields. .extend() on the strict body schema preserves strict
|
|
132
|
+
// mode, so unknown top-level keys still trip the Zod parse before any disk
|
|
133
|
+
// IO — matching the REST handler's "Invalid node patch body" 400 path.
|
|
134
|
+
const PatchNodeInputSchema = NodePatchBodySchema.extend({
|
|
135
|
+
demoId: z.string().min(1),
|
|
136
|
+
nodeId: z.string().min(1),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// add_connector input: { demoId, connector: <connector payload> }. The inner
|
|
140
|
+
// `connector` object is loose (additionalProperties=true via z.record) because
|
|
141
|
+
// DemoSchema runs the full validation server-side after the new connector is
|
|
142
|
+
// merged in (post-mutation parse catches dangling source/target refs and
|
|
143
|
+
// kind-discriminator violations).
|
|
144
|
+
const AddConnectorInputSchema = z.object({
|
|
145
|
+
demoId: z.string().min(1),
|
|
146
|
+
connector: z.record(z.unknown()),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// patch_connector input: { demoId, connectorId } merged with the strict
|
|
150
|
+
// ConnectorPatchBodySchema. .extend() preserves strict mode so unknown
|
|
151
|
+
// top-level keys trip the Zod parse before any IO — matching the REST
|
|
152
|
+
// handler's "Invalid connector patch body" 400 path.
|
|
153
|
+
const PatchConnectorInputSchema = ConnectorPatchBodySchema.extend({
|
|
154
|
+
demoId: z.string().min(1),
|
|
155
|
+
connectorId: z.string().min(1),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const DeleteConnectorInputSchema = z.object({
|
|
159
|
+
demoId: z.string().min(1),
|
|
160
|
+
connectorId: z.string().min(1),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
164
|
+
{
|
|
165
|
+
name: 'seeflow_list_demos',
|
|
166
|
+
description: 'List every demo registered with the studio.',
|
|
167
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
168
|
+
handler: async () => {
|
|
169
|
+
const result = listDemosImpl(deps);
|
|
170
|
+
return okResult(result.data);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'seeflow_get_demo',
|
|
175
|
+
description: 'Get the full demo definition and on-disk state for a demoId.',
|
|
176
|
+
inputSchema: DEMO_ID_INPUT_SCHEMA,
|
|
177
|
+
handler: async (args) => {
|
|
178
|
+
const v = requireDemoId(args);
|
|
179
|
+
if ('error' in v) return errorResult(v.error);
|
|
180
|
+
const result = await getDemoImpl(deps, v.demoId);
|
|
181
|
+
switch (result.kind) {
|
|
182
|
+
case 'ok':
|
|
183
|
+
return okResult(result.data);
|
|
184
|
+
case 'notFound':
|
|
185
|
+
return errorResult('not found');
|
|
186
|
+
case 'fileNotFound':
|
|
187
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'seeflow_register_demo',
|
|
193
|
+
description: 'Register an existing demo file on disk with the studio.',
|
|
194
|
+
inputSchema: inputSchemaFromZod(RegisterBodySchema),
|
|
195
|
+
handler: async (args) => {
|
|
196
|
+
const parsed = RegisterBodySchema.safeParse(args);
|
|
197
|
+
if (!parsed.success) {
|
|
198
|
+
return errorResult(`Invalid register body: ${JSON.stringify(parsed.error.issues)}`);
|
|
199
|
+
}
|
|
200
|
+
const result = await registerDemoImpl(deps, parsed.data);
|
|
201
|
+
switch (result.kind) {
|
|
202
|
+
case 'ok':
|
|
203
|
+
return okResult(result.data);
|
|
204
|
+
case 'fileNotFound':
|
|
205
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
206
|
+
case 'badJson':
|
|
207
|
+
return errorResult(`Demo file is not valid JSON: ${result.detail}`);
|
|
208
|
+
case 'badSchema':
|
|
209
|
+
return errorResult(
|
|
210
|
+
`Demo file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
211
|
+
);
|
|
212
|
+
case 'sdkWriteFailed':
|
|
213
|
+
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'seeflow_delete_demo',
|
|
219
|
+
description: 'Unregister a demo from the studio (the on-disk file is left untouched).',
|
|
220
|
+
inputSchema: DEMO_ID_INPUT_SCHEMA,
|
|
221
|
+
handler: async (args) => {
|
|
222
|
+
const v = requireDemoId(args);
|
|
223
|
+
if ('error' in v) return errorResult(v.error);
|
|
224
|
+
const result = deleteDemoImpl(deps, v.demoId);
|
|
225
|
+
switch (result.kind) {
|
|
226
|
+
case 'ok':
|
|
227
|
+
return okResult({ ok: true });
|
|
228
|
+
case 'notFound':
|
|
229
|
+
return errorResult('not found');
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'seeflow_create_project',
|
|
235
|
+
description: 'Create a new SeeFlow project folder, or register an existing one.',
|
|
236
|
+
inputSchema: inputSchemaFromZod(CreateProjectBodySchema),
|
|
237
|
+
handler: async (args) => {
|
|
238
|
+
const parsed = CreateProjectBodySchema.safeParse(args);
|
|
239
|
+
if (!parsed.success) {
|
|
240
|
+
return errorResult(`Invalid create project body: ${JSON.stringify(parsed.error.issues)}`);
|
|
241
|
+
}
|
|
242
|
+
const result = await createProjectImpl(deps, parsed.data);
|
|
243
|
+
switch (result.kind) {
|
|
244
|
+
case 'ok':
|
|
245
|
+
return okResult(result.data);
|
|
246
|
+
case 'badJson':
|
|
247
|
+
return errorResult(`Existing demo file is not valid JSON: ${result.detail}`);
|
|
248
|
+
case 'badSchema':
|
|
249
|
+
return errorResult(
|
|
250
|
+
`Existing demo file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
251
|
+
);
|
|
252
|
+
case 'scaffoldFailed':
|
|
253
|
+
return errorResult(`Failed to scaffold project: ${result.message}`);
|
|
254
|
+
case 'sdkWriteFailed':
|
|
255
|
+
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'seeflow_add_node',
|
|
261
|
+
description: 'Append a new node to a demo (cascade-safe; id auto-generated when omitted).',
|
|
262
|
+
inputSchema: inputSchemaFromZod(AddNodeInputSchema),
|
|
263
|
+
handler: async (args) => {
|
|
264
|
+
const parsed = AddNodeInputSchema.safeParse(args);
|
|
265
|
+
if (!parsed.success) {
|
|
266
|
+
return errorResult(`Invalid add_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
267
|
+
}
|
|
268
|
+
const { demoId, node } = parsed.data;
|
|
269
|
+
const result = await addNodeImpl(deps, demoId, node);
|
|
270
|
+
switch (result.kind) {
|
|
271
|
+
case 'ok':
|
|
272
|
+
return okResult({ ok: true, id: result.data.id, node: result.data.node });
|
|
273
|
+
case 'demoNotFound':
|
|
274
|
+
return errorResult('unknown demo');
|
|
275
|
+
case 'fileNotFound':
|
|
276
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
277
|
+
case 'badJson':
|
|
278
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
279
|
+
case 'badSchema':
|
|
280
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
281
|
+
case 'writeFailed':
|
|
282
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'seeflow_delete_node',
|
|
288
|
+
description: 'Delete a node and cascade-remove every connector touching it.',
|
|
289
|
+
inputSchema: inputSchemaFromZod(DeleteNodeInputSchema),
|
|
290
|
+
handler: async (args) => {
|
|
291
|
+
const parsed = DeleteNodeInputSchema.safeParse(args);
|
|
292
|
+
if (!parsed.success) {
|
|
293
|
+
return errorResult(`Invalid delete_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
294
|
+
}
|
|
295
|
+
const { demoId, nodeId } = parsed.data;
|
|
296
|
+
const result = await deleteNodeImpl(deps, demoId, nodeId);
|
|
297
|
+
switch (result.kind) {
|
|
298
|
+
case 'ok':
|
|
299
|
+
return okResult({ ok: true });
|
|
300
|
+
case 'demoNotFound':
|
|
301
|
+
return errorResult('unknown demo');
|
|
302
|
+
case 'fileNotFound':
|
|
303
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
304
|
+
case 'badJson':
|
|
305
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
306
|
+
case 'badSchema':
|
|
307
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
308
|
+
case 'unknownNode':
|
|
309
|
+
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
310
|
+
case 'writeFailed':
|
|
311
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'seeflow_move_node',
|
|
317
|
+
description: "Set a node's { x, y } canvas position.",
|
|
318
|
+
inputSchema: inputSchemaFromZod(MoveNodeInputSchema),
|
|
319
|
+
handler: async (args) => {
|
|
320
|
+
const parsed = MoveNodeInputSchema.safeParse(args);
|
|
321
|
+
if (!parsed.success) {
|
|
322
|
+
return errorResult(`Invalid move_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
323
|
+
}
|
|
324
|
+
const { demoId, nodeId, x, y } = parsed.data;
|
|
325
|
+
const result = await moveNodeImpl(deps, demoId, nodeId, { x, y });
|
|
326
|
+
switch (result.kind) {
|
|
327
|
+
case 'ok':
|
|
328
|
+
return okResult({ ok: true, position: result.data.position });
|
|
329
|
+
case 'demoNotFound':
|
|
330
|
+
return errorResult('unknown demo');
|
|
331
|
+
case 'fileNotFound':
|
|
332
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
333
|
+
case 'badJson':
|
|
334
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
335
|
+
case 'badSchema':
|
|
336
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
337
|
+
case 'unknownNode':
|
|
338
|
+
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
339
|
+
case 'writeFailed':
|
|
340
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'seeflow_patch_node',
|
|
346
|
+
description:
|
|
347
|
+
'Update fields on an existing node (position, name, description, detail, colors, border, font, shape, dimensions).',
|
|
348
|
+
inputSchema: inputSchemaFromZod(PatchNodeInputSchema),
|
|
349
|
+
handler: async (args) => {
|
|
350
|
+
const parsed = PatchNodeInputSchema.safeParse(args);
|
|
351
|
+
if (!parsed.success) {
|
|
352
|
+
return errorResult(`Invalid patch_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
353
|
+
}
|
|
354
|
+
const { demoId, nodeId, ...updates } = parsed.data;
|
|
355
|
+
const result = await patchNodeImpl(deps, demoId, nodeId, updates);
|
|
356
|
+
switch (result.kind) {
|
|
357
|
+
case 'ok':
|
|
358
|
+
return okResult({ ok: true });
|
|
359
|
+
case 'demoNotFound':
|
|
360
|
+
return errorResult('unknown demo');
|
|
361
|
+
case 'fileNotFound':
|
|
362
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
363
|
+
case 'badJson':
|
|
364
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
365
|
+
case 'badSchema':
|
|
366
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
367
|
+
case 'unknownNode':
|
|
368
|
+
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
369
|
+
case 'writeFailed':
|
|
370
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'seeflow_reorder_node',
|
|
376
|
+
description:
|
|
377
|
+
'Reorder a node within demo.nodes[] (forward / backward / toFront / toBack / toIndex).',
|
|
378
|
+
inputSchema: inputSchemaFromZod(ReorderNodeInputSchema),
|
|
379
|
+
handler: async (args) => {
|
|
380
|
+
const parsed = ReorderNodeInputSchema.safeParse(args);
|
|
381
|
+
if (!parsed.success) {
|
|
382
|
+
return errorResult(
|
|
383
|
+
`Invalid reorder_node arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const { demoId, nodeId, ...body } = parsed.data;
|
|
387
|
+
// Delegate the op-specific shape to the existing ReorderBodySchema so
|
|
388
|
+
// reorderNodeImpl receives the same discriminated union the REST route
|
|
389
|
+
// does — keeps a single source of truth for op semantics.
|
|
390
|
+
const reorderBody = ReorderBodySchema.parse(body);
|
|
391
|
+
const result = await reorderNodeImpl(deps, demoId, nodeId, reorderBody);
|
|
392
|
+
switch (result.kind) {
|
|
393
|
+
case 'ok':
|
|
394
|
+
return okResult({ ok: true });
|
|
395
|
+
case 'demoNotFound':
|
|
396
|
+
return errorResult('unknown demo');
|
|
397
|
+
case 'fileNotFound':
|
|
398
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
399
|
+
case 'badJson':
|
|
400
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
401
|
+
case 'badSchema':
|
|
402
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
403
|
+
case 'unknownNode':
|
|
404
|
+
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
405
|
+
case 'writeFailed':
|
|
406
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: 'seeflow_add_connector',
|
|
412
|
+
description:
|
|
413
|
+
"Append a new connector between two nodes (kind defaults to 'default'; id auto-generated when omitted).",
|
|
414
|
+
inputSchema: inputSchemaFromZod(AddConnectorInputSchema),
|
|
415
|
+
handler: async (args) => {
|
|
416
|
+
const parsed = AddConnectorInputSchema.safeParse(args);
|
|
417
|
+
if (!parsed.success) {
|
|
418
|
+
return errorResult(
|
|
419
|
+
`Invalid add_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const { demoId, connector } = parsed.data;
|
|
423
|
+
const result = await addConnectorImpl(deps, demoId, connector);
|
|
424
|
+
switch (result.kind) {
|
|
425
|
+
case 'ok':
|
|
426
|
+
return okResult({ ok: true, id: result.data.id });
|
|
427
|
+
case 'demoNotFound':
|
|
428
|
+
return errorResult('unknown demo');
|
|
429
|
+
case 'fileNotFound':
|
|
430
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
431
|
+
case 'badJson':
|
|
432
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
433
|
+
case 'badSchema':
|
|
434
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
435
|
+
case 'writeFailed':
|
|
436
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'seeflow_patch_connector',
|
|
442
|
+
description:
|
|
443
|
+
'Update fields on an existing connector (label, style, color, kind, per-kind payload, reconnect endpoints).',
|
|
444
|
+
inputSchema: inputSchemaFromZod(PatchConnectorInputSchema),
|
|
445
|
+
handler: async (args) => {
|
|
446
|
+
const parsed = PatchConnectorInputSchema.safeParse(args);
|
|
447
|
+
if (!parsed.success) {
|
|
448
|
+
return errorResult(
|
|
449
|
+
`Invalid patch_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const { demoId, connectorId, ...updates } = parsed.data;
|
|
453
|
+
const result = await patchConnectorImpl(deps, demoId, connectorId, updates);
|
|
454
|
+
switch (result.kind) {
|
|
455
|
+
case 'ok':
|
|
456
|
+
return okResult({ ok: true });
|
|
457
|
+
case 'demoNotFound':
|
|
458
|
+
return errorResult('unknown demo');
|
|
459
|
+
case 'fileNotFound':
|
|
460
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
461
|
+
case 'badJson':
|
|
462
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
463
|
+
case 'badSchema':
|
|
464
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
465
|
+
case 'unknownConnector':
|
|
466
|
+
return errorResult(`Unknown connectorId: ${connectorId}`);
|
|
467
|
+
case 'writeFailed':
|
|
468
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'seeflow_delete_connector',
|
|
474
|
+
description: 'Delete a connector by id.',
|
|
475
|
+
inputSchema: inputSchemaFromZod(DeleteConnectorInputSchema),
|
|
476
|
+
handler: async (args) => {
|
|
477
|
+
const parsed = DeleteConnectorInputSchema.safeParse(args);
|
|
478
|
+
if (!parsed.success) {
|
|
479
|
+
return errorResult(
|
|
480
|
+
`Invalid delete_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const { demoId, connectorId } = parsed.data;
|
|
484
|
+
const result = await deleteConnectorImpl(deps, demoId, connectorId);
|
|
485
|
+
switch (result.kind) {
|
|
486
|
+
case 'ok':
|
|
487
|
+
return okResult({ ok: true });
|
|
488
|
+
case 'demoNotFound':
|
|
489
|
+
return errorResult('unknown demo');
|
|
490
|
+
case 'fileNotFound':
|
|
491
|
+
return errorResult(`Demo file not found: ${result.path}`);
|
|
492
|
+
case 'badJson':
|
|
493
|
+
return errorResult(`Demo file is not valid JSON: ${result.message}`);
|
|
494
|
+
case 'badSchema':
|
|
495
|
+
return errorResult(`Demo failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
496
|
+
case 'unknownConnector':
|
|
497
|
+
return errorResult(`Unknown connectorId: ${connectorId}`);
|
|
498
|
+
case 'writeFailed':
|
|
499
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Build a fresh MCP Server scoped to a registry + watcher. The server speaks
|
|
507
|
+
* `tools/list` and `tools/call` against the tool list. Wired to a transport
|
|
508
|
+
* by the caller (see the /mcp route in server.ts and the stdio shim in
|
|
509
|
+
* mcp-shim.ts) — every request builds its own server in stateless mode.
|
|
510
|
+
*/
|
|
511
|
+
export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
512
|
+
const tools = buildTools({
|
|
513
|
+
registry: options.registry,
|
|
514
|
+
watcher: options.watcher,
|
|
515
|
+
projectBaseDir: options.projectBaseDir,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const server = new Server({ name: 'seeflow', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
519
|
+
|
|
520
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
521
|
+
tools: tools.map(({ name, description, inputSchema }) => ({
|
|
522
|
+
name,
|
|
523
|
+
description,
|
|
524
|
+
inputSchema,
|
|
525
|
+
})),
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
529
|
+
const tool = tools.find((t) => t.name === request.params.name);
|
|
530
|
+
if (!tool) {
|
|
531
|
+
return {
|
|
532
|
+
isError: true,
|
|
533
|
+
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return tool.handler(request.params.arguments);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
return server;
|
|
540
|
+
}
|