mcpgraph-ux 0.1.2 → 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 CHANGED
@@ -6,6 +6,8 @@ A Next.js application that provides a visual interface for the [mcpGraph](https:
6
6
  - **List tools**: See all available MCP tools defined in your graph configuration
7
7
  - **Test tools**: Execute tools with custom parameters and view results from the exit node
8
8
 
9
+ ![mcpGraph UX Screenshot](screenshot.png)
10
+
9
11
  ## Features
10
12
 
11
13
  - 🎨 **Graph Visualization**: Interactive graph visualization using React Flow, showing all nodes and their connections
@@ -0,0 +1,106 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getController } from '@/lib/executionController';
3
+
4
+ // Force dynamic rendering
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ // Store breakpoints by executionId (fallback if controller not available)
8
+ const breakpointsStore = new Map<string, string[]>();
9
+
10
+ export async function GET(request: NextRequest) {
11
+ try {
12
+ const executionId = request.nextUrl.searchParams.get('executionId');
13
+
14
+ if (!executionId) {
15
+ return NextResponse.json(
16
+ { error: 'Missing executionId parameter' },
17
+ { status: 400 }
18
+ );
19
+ }
20
+
21
+ const controller = getController(executionId);
22
+ if (controller) {
23
+ // Get breakpoints from controller if available
24
+ const state = controller.getState();
25
+ // Note: mcpGraph doesn't expose breakpoints directly, so we use our store
26
+ const breakpoints = breakpointsStore.get(executionId) || [];
27
+ return NextResponse.json({ breakpoints });
28
+ }
29
+
30
+ // Fallback to store
31
+ const breakpoints = breakpointsStore.get(executionId) || [];
32
+ return NextResponse.json({ breakpoints });
33
+ } catch (error) {
34
+ console.error('Error getting breakpoints:', error);
35
+ return NextResponse.json(
36
+ { error: error instanceof Error ? error.message : 'Unknown error' },
37
+ { status: 500 }
38
+ );
39
+ }
40
+ }
41
+
42
+ export async function POST(request: NextRequest) {
43
+ try {
44
+ const body = await request.json();
45
+ const { executionId, breakpoints } = body;
46
+
47
+ if (!executionId) {
48
+ return NextResponse.json(
49
+ { error: 'Missing executionId' },
50
+ { status: 400 }
51
+ );
52
+ }
53
+
54
+ if (!Array.isArray(breakpoints)) {
55
+ return NextResponse.json(
56
+ { error: 'breakpoints must be an array' },
57
+ { status: 400 }
58
+ );
59
+ }
60
+
61
+ const controller = getController(executionId);
62
+ if (controller) {
63
+ controller.setBreakpoints(breakpoints);
64
+ }
65
+
66
+ // Store breakpoints
67
+ breakpointsStore.set(executionId, breakpoints);
68
+
69
+ return NextResponse.json({ success: true, breakpoints });
70
+ } catch (error) {
71
+ console.error('Error setting breakpoints:', error);
72
+ return NextResponse.json(
73
+ { error: error instanceof Error ? error.message : 'Unknown error' },
74
+ { status: 500 }
75
+ );
76
+ }
77
+ }
78
+
79
+ export async function DELETE(request: NextRequest) {
80
+ try {
81
+ const executionId = request.nextUrl.searchParams.get('executionId');
82
+
83
+ if (!executionId) {
84
+ return NextResponse.json(
85
+ { error: 'Missing executionId parameter' },
86
+ { status: 400 }
87
+ );
88
+ }
89
+
90
+ const controller = getController(executionId);
91
+ if (controller) {
92
+ controller.clearBreakpoints();
93
+ }
94
+
95
+ breakpointsStore.delete(executionId);
96
+
97
+ return NextResponse.json({ success: true });
98
+ } catch (error) {
99
+ console.error('Error clearing breakpoints:', error);
100
+ return NextResponse.json(
101
+ { error: error instanceof Error ? error.message : 'Unknown error' },
102
+ { status: 500 }
103
+ );
104
+ }
105
+ }
106
+
@@ -0,0 +1,69 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getController } from '@/lib/executionController';
3
+ import { getApi } from '@/lib/mcpGraphApi';
4
+
5
+ // Force dynamic rendering
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ const executionId = request.nextUrl.searchParams.get('executionId');
11
+ const nodeId = request.nextUrl.searchParams.get('nodeId');
12
+ const sequenceIdParam = request.nextUrl.searchParams.get('sequenceId');
13
+
14
+ if (!executionId) {
15
+ return NextResponse.json(
16
+ { error: 'Missing executionId parameter' },
17
+ { status: 400 }
18
+ );
19
+ }
20
+
21
+ if (!nodeId || !sequenceIdParam) {
22
+ return NextResponse.json(
23
+ { error: 'Missing nodeId or sequenceId parameter' },
24
+ { status: 400 }
25
+ );
26
+ }
27
+
28
+ const sequenceId = parseInt(sequenceIdParam, 10);
29
+ if (isNaN(sequenceId)) {
30
+ return NextResponse.json(
31
+ { error: 'Invalid sequenceId parameter' },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ // Try to get controller first (for active executions)
37
+ const controller = getController(executionId);
38
+ let context: Record<string, unknown> | null = null;
39
+
40
+ if (controller) {
41
+ // Use controller's context directly (works for active executions)
42
+ try {
43
+ const state = controller.getState();
44
+ context = state.context.getContextForExecution(sequenceId);
45
+ console.log(`[API] Got context from controller for executionIndex=${sequenceId}, nodeId=${nodeId}:`, context ? 'present' : 'null');
46
+ } catch (error) {
47
+ console.error(`[API] Error getting context from controller:`, error);
48
+ }
49
+ } else {
50
+ // Fallback to API method (may not work after execution completes)
51
+ const api = getApi();
52
+ if (api) {
53
+ context = api.getContextForExecution(sequenceId);
54
+ console.log(`[API] Got context from API for executionIndex=${sequenceId}, nodeId=${nodeId}:`, context ? 'present' : 'null');
55
+ } else {
56
+ console.error(`[API] No controller or API available`);
57
+ }
58
+ }
59
+
60
+ return NextResponse.json({ context: context || null });
61
+ } catch (error) {
62
+ console.error('Error getting node input context:', error);
63
+ return NextResponse.json(
64
+ { error: error instanceof Error ? error.message : 'Unknown error' },
65
+ { status: 500 }
66
+ );
67
+ }
68
+ }
69
+
@@ -0,0 +1,121 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getController, unregisterController, getAllExecutionIds } from '@/lib/executionController';
3
+ import { sendExecutionEvent, closeExecutionStream } from '@/lib/executionStreamServer';
4
+ import { getApi } from '@/lib/mcpGraphApi';
5
+
6
+
7
+ export async function POST(request: NextRequest) {
8
+ try {
9
+ const body = await request.json();
10
+ const { executionId, action } = body;
11
+
12
+ if (!executionId) {
13
+ return NextResponse.json(
14
+ { error: 'Missing executionId' },
15
+ { status: 400 }
16
+ );
17
+ }
18
+
19
+ if (!action || !['pause', 'resume', 'step', 'stop'].includes(action)) {
20
+ return NextResponse.json(
21
+ { error: 'Invalid action. Must be one of: pause, resume, step, stop' },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ console.log(`[Controller API] Looking for controller with executionId: ${executionId}`);
27
+ // Force getApi to be called to ensure module is loaded
28
+ const api = getApi();
29
+ console.log(`[Controller API] API instance: ${api ? 'present' : 'null'}`);
30
+ const controller = getController(executionId);
31
+ if (!controller) {
32
+ console.log(`[Controller API] Controller not found for executionId: ${executionId}`);
33
+ // Log all registered executionIds for debugging
34
+ const allIds = getAllExecutionIds();
35
+ console.log(`[Controller API] Currently registered executionIds: ${allIds.length > 0 ? allIds.join(', ') : 'none'}`);
36
+ return NextResponse.json(
37
+ { error: 'Execution not found or not active' },
38
+ { status: 404 }
39
+ );
40
+ }
41
+ console.log(`[Controller API] Found controller for executionId: ${executionId}, status: ${controller.getState().status}`);
42
+
43
+ const state = controller.getState();
44
+
45
+ switch (action) {
46
+ case 'pause':
47
+ if (state.status !== 'running') {
48
+ return NextResponse.json(
49
+ { error: `Cannot pause: execution status is ${state.status}` },
50
+ { status: 400 }
51
+ );
52
+ }
53
+ controller.pause();
54
+ // Don't send stateUpdate here - onPause hook will send it
55
+ return NextResponse.json({ success: true, status: 'paused' });
56
+
57
+ case 'resume':
58
+ if (state.status !== 'paused') {
59
+ return NextResponse.json(
60
+ { error: `Cannot resume: execution status is ${state.status}` },
61
+ { status: 400 }
62
+ );
63
+ }
64
+ controller.resume();
65
+ // Don't send stateUpdate here - the onResume hook already sends it
66
+ return NextResponse.json({ success: true, status: 'running' });
67
+
68
+ case 'step':
69
+ if (state.status !== 'paused') {
70
+ return NextResponse.json(
71
+ { error: `Cannot step: execution status is ${state.status}` },
72
+ { status: 400 }
73
+ );
74
+ }
75
+ await controller.step();
76
+ const newState = controller.getState();
77
+ // Don't send stateUpdate here - the onPause hook already sends the correct stateUpdate
78
+ // when step completes and pauses at the next node
79
+ return NextResponse.json({
80
+ success: true,
81
+ status: newState.status,
82
+ currentNodeId: newState.currentNodeId,
83
+ });
84
+
85
+ case 'stop':
86
+ if (state.status !== 'running' && state.status !== 'paused') {
87
+ return NextResponse.json(
88
+ { error: `Cannot stop: execution status is ${state.status}` },
89
+ { status: 400 }
90
+ );
91
+ }
92
+
93
+ // Call stop() - this sets status to "stopped" and will cause execution to throw "Execution was stopped"
94
+ controller.stop();
95
+
96
+ // Send stopped event
97
+ sendExecutionEvent(executionId, 'executionStopped', {
98
+ timestamp: Date.now(),
99
+ });
100
+
101
+ // Clean up controller and stream
102
+ unregisterController(executionId);
103
+ closeExecutionStream(executionId);
104
+
105
+ return NextResponse.json({ success: true, status: 'stopped' });
106
+
107
+ default:
108
+ return NextResponse.json(
109
+ { error: 'Invalid action' },
110
+ { status: 400 }
111
+ );
112
+ }
113
+ } catch (error) {
114
+ console.error('Error in controller action:', error);
115
+ return NextResponse.json(
116
+ { error: error instanceof Error ? error.message : 'Unknown error' },
117
+ { status: 500 }
118
+ );
119
+ }
120
+ }
121
+
@@ -0,0 +1,40 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getController } from '@/lib/executionController';
3
+ import { getApi } from '@/lib/mcpGraphApi';
4
+
5
+ // Force dynamic rendering
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ const executionId = request.nextUrl.searchParams.get('executionId');
11
+
12
+ if (!executionId) {
13
+ return NextResponse.json(
14
+ { error: 'Missing executionId parameter' },
15
+ { status: 400 }
16
+ );
17
+ }
18
+
19
+ const controller = getController(executionId);
20
+ if (!controller) {
21
+ return NextResponse.json(
22
+ { error: 'Execution not found or not active' },
23
+ { status: 404 }
24
+ );
25
+ }
26
+
27
+ // Get execution history from the controller's state
28
+ const state = controller.getState();
29
+ const history = state.executionHistory || [];
30
+
31
+ return NextResponse.json({ history });
32
+ } catch (error) {
33
+ console.error('Error getting execution history:', error);
34
+ return NextResponse.json(
35
+ { error: error instanceof Error ? error.message : 'Unknown error' },
36
+ { status: 500 }
37
+ );
38
+ }
39
+ }
40
+
@@ -0,0 +1,57 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getController } from '@/lib/executionController';
3
+ import { getApi } from '@/lib/mcpGraphApi';
4
+ import type { NodeExecutionRecord } from 'mcpgraph';
5
+
6
+ // Force dynamic rendering
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ export async function GET(request: NextRequest) {
10
+ try {
11
+ const executionId = request.nextUrl.searchParams.get('executionId');
12
+
13
+ if (!executionId) {
14
+ return NextResponse.json(
15
+ { error: 'Missing executionId parameter' },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ const controller = getController(executionId);
21
+ if (!controller) {
22
+ return NextResponse.json(
23
+ { error: 'Execution not found or not active' },
24
+ { status: 404 }
25
+ );
26
+ }
27
+
28
+ const api = getApi();
29
+ if (!api) {
30
+ return NextResponse.json(
31
+ { error: 'API instance not available' },
32
+ { status: 500 }
33
+ );
34
+ }
35
+
36
+ // Get execution history by iterating through execution records
37
+ // We'll get records until we hit null
38
+ const history: NodeExecutionRecord[] = [];
39
+ let index = 0;
40
+
41
+ while (true) {
42
+ const record = api.getExecutionByIndex(index);
43
+ if (!record) break;
44
+ history.push(record);
45
+ index++;
46
+ }
47
+
48
+ return NextResponse.json({ history });
49
+ } catch (error) {
50
+ console.error('Error getting execution history with indices:', error);
51
+ return NextResponse.json(
52
+ { error: error instanceof Error ? error.message : 'Unknown error' },
53
+ { status: 500 }
54
+ );
55
+ }
56
+ }
57
+
@@ -0,0 +1,34 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { registerExecutionStream, unregisterExecutionStream } from '@/lib/executionStreamServer';
3
+
4
+
5
+ export async function GET(request: NextRequest) {
6
+ const executionId = request.nextUrl.searchParams.get('executionId');
7
+
8
+ if (!executionId) {
9
+ return new Response('Missing executionId parameter', { status: 400 });
10
+ }
11
+
12
+ const stream = new ReadableStream({
13
+ start(controller) {
14
+ // Register stream for this execution
15
+ registerExecutionStream(executionId, controller);
16
+
17
+ // Note: The actual execution will be started via POST /api/tools/[toolName]
18
+ // This stream will receive events from those hooks
19
+ },
20
+ cancel() {
21
+ // Clean up when client disconnects
22
+ unregisterExecutionStream(executionId);
23
+ },
24
+ });
25
+
26
+ return new Response(stream, {
27
+ headers: {
28
+ 'Content-Type': 'text/event-stream',
29
+ 'Cache-Control': 'no-cache',
30
+ 'Connection': 'keep-alive',
31
+ },
32
+ });
33
+ }
34
+
@@ -1,24 +1,10 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { McpGraphApi, type NodeDefinition } from 'mcpgraph';
2
+ import { type NodeDefinition } from 'mcpgraph';
3
+ import { getApi } from '@/lib/mcpGraphApi';
3
4
 
4
5
  // Force dynamic rendering - this route requires runtime config
5
6
  export const dynamic = 'force-dynamic';
6
7
 
7
- let apiInstance: McpGraphApi | null = null;
8
-
9
- function getApi(): McpGraphApi {
10
- const configPath = process.env.MCPGRAPH_CONFIG_PATH;
11
- if (!configPath) {
12
- throw new Error('MCPGRAPH_CONFIG_PATH environment variable is not set');
13
- }
14
-
15
- if (!apiInstance) {
16
- apiInstance = new McpGraphApi(configPath);
17
- }
18
-
19
- return apiInstance;
20
- }
21
-
22
8
  export async function GET() {
23
9
  try {
24
10
  const api = getApi();
@@ -37,39 +23,47 @@ export async function GET() {
37
23
  position: { x: 0, y: 0 }, // Will be calculated by layout algorithm
38
24
  };
39
25
 
40
- // Add specific data based on node type
41
- if (node.type === 'entry' || node.type === 'exit') {
26
+ // Add specific data based on node type using type guards
27
+ if (node.type === 'entry' && 'tool' in node) {
42
28
  return {
43
29
  ...baseNode,
44
30
  data: {
45
31
  ...baseNode.data,
46
- tool: (node as any).tool,
32
+ tool: node.tool,
47
33
  },
48
34
  };
49
- } else if (node.type === 'mcp') {
35
+ } else if (node.type === 'exit' && 'tool' in node) {
50
36
  return {
51
37
  ...baseNode,
52
38
  data: {
53
39
  ...baseNode.data,
54
- server: (node as any).server,
55
- tool: (node as any).tool,
56
- args: (node as any).args,
40
+ tool: node.tool,
57
41
  },
58
42
  };
59
- } else if (node.type === 'transform') {
43
+ } else if (node.type === 'mcp' && 'server' in node && 'tool' in node && 'args' in node) {
60
44
  return {
61
45
  ...baseNode,
62
46
  data: {
63
47
  ...baseNode.data,
64
- transform: (node as any).transform,
48
+ server: node.server,
49
+ tool: node.tool,
50
+ args: node.args,
65
51
  },
66
52
  };
67
- } else if (node.type === 'switch') {
53
+ } else if (node.type === 'transform' && 'transform' in node) {
68
54
  return {
69
55
  ...baseNode,
70
56
  data: {
71
57
  ...baseNode.data,
72
- conditions: (node as any).conditions,
58
+ transform: node.transform,
59
+ },
60
+ };
61
+ } else if (node.type === 'switch' && 'conditions' in node) {
62
+ return {
63
+ ...baseNode,
64
+ data: {
65
+ ...baseNode.data,
66
+ conditions: node.conditions,
73
67
  },
74
68
  };
75
69
  }
@@ -90,8 +84,7 @@ export async function GET() {
90
84
  }
91
85
 
92
86
  if (node.type === 'switch' && 'conditions' in node) {
93
- const switchNode = node as any;
94
- switchNode.conditions.forEach((condition: any, index: number) => {
87
+ node.conditions.forEach((condition, index: number) => {
95
88
  edges.push({
96
89
  id: `${node.id}-${condition.target}-${index}`,
97
90
  source: node.id,
@@ -102,7 +95,45 @@ export async function GET() {
102
95
  }
103
96
  });
104
97
 
105
- return NextResponse.json({ nodes, edges, tools: config.tools });
98
+ return NextResponse.json({
99
+ nodes,
100
+ edges,
101
+ tools: config.tools,
102
+ config: {
103
+ name: config.server.name,
104
+ version: config.server.version,
105
+ description: config.server.description,
106
+ servers: Object.entries(config.servers || {}).map(([name, server]) => {
107
+ const details: {
108
+ name: string;
109
+ type: string;
110
+ command?: string;
111
+ args?: string[];
112
+ cwd?: string;
113
+ url?: string;
114
+ headers?: Record<string, string>;
115
+ } = {
116
+ name,
117
+ type: server.type || 'stdio',
118
+ };
119
+
120
+ if (server.type === 'stdio' || !server.type) {
121
+ details.command = server.command;
122
+ details.args = server.args || [];
123
+ if (server.cwd) {
124
+ details.cwd = server.cwd;
125
+ }
126
+ } else if (server.type === 'sse' || server.type === 'streamableHttp') {
127
+ details.url = server.url;
128
+ if (server.headers) {
129
+ details.headers = server.headers;
130
+ }
131
+ }
132
+
133
+ return details;
134
+ }),
135
+ },
136
+ });
106
137
  } catch (error) {
107
138
  console.error('Error getting graph:', error);
108
139
  return NextResponse.json(