mcpgraph-ux 0.1.2 → 0.1.4

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
@@ -104,3 +106,7 @@ This application follows the design recommendations from the mcpGraph project:
104
106
  ## License
105
107
 
106
108
  MIT
109
+
110
+ ## TODO
111
+
112
+ Would be nice to be able to inspect nodes in graph
@@ -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,31 +1,51 @@
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
- export async function GET() {
8
+ export async function GET(request: Request) {
23
9
  try {
24
10
  const api = getApi();
25
11
  const config = api.getConfig();
26
12
 
13
+ // Get toolName from query parameter
14
+ const url = new URL(request.url);
15
+ const toolName = url.searchParams.get('toolName');
16
+
17
+ if (!toolName) {
18
+ return NextResponse.json(
19
+ { error: 'toolName query parameter is required' },
20
+ { status: 400 }
21
+ );
22
+ }
23
+
24
+ // Get tool definition from config.tools (which is ToolDefinition[], not ToolInfo)
25
+ // getTool() returns ToolInfo which doesn't have nodes - we need ToolDefinition from config
26
+ const tool = config.tools.find(t => t.name === toolName);
27
+
28
+ if (!tool) {
29
+ const toolNames = config.tools.map(t => t.name);
30
+ return NextResponse.json(
31
+ { error: `Tool '${toolName}' not found. Available tools: ${toolNames.join(', ')}` },
32
+ { status: 404 }
33
+ );
34
+ }
35
+
36
+ if (!tool.nodes || tool.nodes.length === 0) {
37
+ return NextResponse.json(
38
+ { error: `Tool '${toolName}' has no nodes defined` },
39
+ { status: 404 }
40
+ );
41
+ }
42
+
43
+ const allNodes: NodeDefinition[] = tool.nodes;
44
+ console.log(`[graph/route] Found ${allNodes.length} nodes for tool '${toolName}'`);
45
+ console.log(`[graph/route] Node IDs:`, allNodes.map(n => n.id));
46
+
27
47
  // Transform nodes into React Flow format
28
- const nodes = config.nodes.map((node: NodeDefinition) => {
48
+ const nodes = allNodes.map((node: NodeDefinition) => {
29
49
  const baseNode = {
30
50
  id: node.id,
31
51
  type: node.type,
@@ -37,39 +57,47 @@ export async function GET() {
37
57
  position: { x: 0, y: 0 }, // Will be calculated by layout algorithm
38
58
  };
39
59
 
40
- // Add specific data based on node type
41
- if (node.type === 'entry' || node.type === 'exit') {
60
+ // Add specific data based on node type using type guards
61
+ if (node.type === 'entry' && 'tool' in node) {
62
+ return {
63
+ ...baseNode,
64
+ data: {
65
+ ...baseNode.data,
66
+ tool: node.tool,
67
+ },
68
+ };
69
+ } else if (node.type === 'exit' && 'tool' in node) {
42
70
  return {
43
71
  ...baseNode,
44
72
  data: {
45
73
  ...baseNode.data,
46
- tool: (node as any).tool,
74
+ tool: node.tool,
47
75
  },
48
76
  };
49
- } else if (node.type === 'mcp') {
77
+ } else if (node.type === 'mcp' && 'server' in node && 'tool' in node && 'args' in node) {
50
78
  return {
51
79
  ...baseNode,
52
80
  data: {
53
81
  ...baseNode.data,
54
- server: (node as any).server,
55
- tool: (node as any).tool,
56
- args: (node as any).args,
82
+ server: node.server,
83
+ tool: node.tool,
84
+ args: node.args,
57
85
  },
58
86
  };
59
- } else if (node.type === 'transform') {
87
+ } else if (node.type === 'transform' && 'transform' in node) {
60
88
  return {
61
89
  ...baseNode,
62
90
  data: {
63
91
  ...baseNode.data,
64
- transform: (node as any).transform,
92
+ transform: node.transform,
65
93
  },
66
94
  };
67
- } else if (node.type === 'switch') {
95
+ } else if (node.type === 'switch' && 'conditions' in node) {
68
96
  return {
69
97
  ...baseNode,
70
98
  data: {
71
99
  ...baseNode.data,
72
- conditions: (node as any).conditions,
100
+ conditions: node.conditions,
73
101
  },
74
102
  };
75
103
  }
@@ -80,7 +108,7 @@ export async function GET() {
80
108
  // Create edges from node.next and switch conditions
81
109
  const edges: Array<{ id: string; source: string; target: string; label?: string }> = [];
82
110
 
83
- config.nodes.forEach((node: NodeDefinition) => {
111
+ allNodes.forEach((node: NodeDefinition) => {
84
112
  if ('next' in node && node.next) {
85
113
  edges.push({
86
114
  id: `${node.id}-${node.next}`,
@@ -90,8 +118,7 @@ export async function GET() {
90
118
  }
91
119
 
92
120
  if (node.type === 'switch' && 'conditions' in node) {
93
- const switchNode = node as any;
94
- switchNode.conditions.forEach((condition: any, index: number) => {
121
+ node.conditions.forEach((condition, index: number) => {
95
122
  edges.push({
96
123
  id: `${node.id}-${condition.target}-${index}`,
97
124
  source: node.id,
@@ -102,7 +129,46 @@ export async function GET() {
102
129
  }
103
130
  });
104
131
 
105
- return NextResponse.json({ nodes, edges, tools: config.tools });
132
+ console.log(`[graph/route] Returning ${nodes.length} nodes and ${edges.length} edges`);
133
+
134
+ return NextResponse.json({
135
+ nodes,
136
+ edges,
137
+ tools: config.tools,
138
+ config: {
139
+ name: config.server.name,
140
+ version: config.server.version,
141
+ servers: Object.entries(config.mcpServers || {}).map(([name, server]) => {
142
+ const details: {
143
+ name: string;
144
+ type: string;
145
+ command?: string;
146
+ args?: string[];
147
+ cwd?: string;
148
+ url?: string;
149
+ headers?: Record<string, string>;
150
+ } = {
151
+ name,
152
+ type: server.type || 'stdio',
153
+ };
154
+
155
+ if (server.type === 'stdio' || !server.type) {
156
+ details.command = server.command;
157
+ details.args = server.args || [];
158
+ if (server.cwd) {
159
+ details.cwd = server.cwd;
160
+ }
161
+ } else if (server.type === 'sse' || server.type === 'streamableHttp') {
162
+ details.url = server.url;
163
+ if (server.headers) {
164
+ details.headers = server.headers;
165
+ }
166
+ }
167
+
168
+ return details;
169
+ }),
170
+ },
171
+ });
106
172
  } catch (error) {
107
173
  console.error('Error getting graph:', error);
108
174
  return NextResponse.json(