mcpgraph-ux 0.1.3 → 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
@@ -106,3 +106,7 @@ This application follows the design recommendations from the mcpGraph project:
106
106
  ## License
107
107
 
108
108
  MIT
109
+
110
+ ## TODO
111
+
112
+ Would be nice to be able to inspect nodes in graph
@@ -5,13 +5,47 @@ import { getApi } from '@/lib/mcpGraphApi';
5
5
  // Force dynamic rendering - this route requires runtime config
6
6
  export const dynamic = 'force-dynamic';
7
7
 
8
- export async function GET() {
8
+ export async function GET(request: Request) {
9
9
  try {
10
10
  const api = getApi();
11
11
  const config = api.getConfig();
12
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
+
13
47
  // Transform nodes into React Flow format
14
- const nodes = config.nodes.map((node: NodeDefinition) => {
48
+ const nodes = allNodes.map((node: NodeDefinition) => {
15
49
  const baseNode = {
16
50
  id: node.id,
17
51
  type: node.type,
@@ -74,7 +108,7 @@ export async function GET() {
74
108
  // Create edges from node.next and switch conditions
75
109
  const edges: Array<{ id: string; source: string; target: string; label?: string }> = [];
76
110
 
77
- config.nodes.forEach((node: NodeDefinition) => {
111
+ allNodes.forEach((node: NodeDefinition) => {
78
112
  if ('next' in node && node.next) {
79
113
  edges.push({
80
114
  id: `${node.id}-${node.next}`,
@@ -95,6 +129,8 @@ export async function GET() {
95
129
  }
96
130
  });
97
131
 
132
+ console.log(`[graph/route] Returning ${nodes.length} nodes and ${edges.length} edges`);
133
+
98
134
  return NextResponse.json({
99
135
  nodes,
100
136
  edges,
@@ -102,8 +138,7 @@ export async function GET() {
102
138
  config: {
103
139
  name: config.server.name,
104
140
  version: config.server.version,
105
- description: config.server.description,
106
- servers: Object.entries(config.servers || {}).map(([name, server]) => {
141
+ servers: Object.entries(config.mcpServers || {}).map(([name, server]) => {
107
142
  const details: {
108
143
  name: string;
109
144
  type: string;
@@ -1,5 +1,12 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { type ExecutionOptions, type ExecutionHooks, type ExecutionResult } from 'mcpgraph';
2
+ import {
3
+ type ExecutionOptions,
4
+ type ExecutionHooks,
5
+ type ExecutionResult,
6
+ type NodeDefinition,
7
+ ToolCallMcpError,
8
+ ToolCallError
9
+ } from 'mcpgraph';
3
10
  import { getApi } from '@/lib/mcpGraphApi';
4
11
  import { sendExecutionEvent, closeExecutionStream } from '@/lib/executionStreamServer';
5
12
  import { registerController, unregisterController, getController } from '@/lib/executionController';
@@ -37,10 +44,14 @@ export async function POST(
37
44
  let executionId: string | undefined;
38
45
  try {
39
46
  const api = getApi();
40
- const body = await request.json();
47
+ const body = await request.json() as {
48
+ args?: Record<string, unknown>;
49
+ executionId?: string;
50
+ options?: ExecutionOptions;
51
+ };
41
52
  const args = body.args || {};
42
- executionId = body.executionId as string | undefined;
43
- const executionOptions = body.options as ExecutionOptions | undefined;
53
+ executionId = body.executionId;
54
+ const executionOptions = body.options;
44
55
 
45
56
  // Log breakpoints received
46
57
  const breakpointsReceived = executionOptions?.breakpoints || [];
@@ -93,26 +104,84 @@ export async function POST(
93
104
  });
94
105
  },
95
106
  onNodeError: async (executionIndex, nodeId, node, error, context) => {
107
+ // Log the entire error object for debugging
108
+ console.log(`[API] onNodeError - Full error object dump:`);
109
+ console.log(`[API] Error type: ${error.constructor.name}`);
110
+ console.log(`[API] Error instanceof ToolCallMcpError: ${error instanceof ToolCallMcpError}`);
111
+ console.log(`[API] Error instanceof ToolCallError: ${error instanceof ToolCallError}`);
112
+ console.log(`[API] Error instanceof Error: ${error instanceof Error}`);
113
+ console.log(`[API] Error object keys:`, Object.keys(error));
114
+ console.log(`[API] Full error object:`, JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
115
+ console.log(`[API] Error properties:`, {
116
+ name: error.name,
117
+ message: error.message,
118
+ stack: error.stack,
119
+ code: (error as any).code,
120
+ data: (error as any).data,
121
+ stderr: (error as any).stderr,
122
+ result: (error as any).result,
123
+ });
124
+
125
+ // Type-safe extraction of error properties based on error type
126
+ const errorData: {
127
+ message: string;
128
+ stack?: string;
129
+ code?: number;
130
+ data?: unknown;
131
+ stderr?: string[]; // For ToolCallMcpError
132
+ result?: unknown; // For ToolCallError
133
+ errorType: 'mcp' | 'tool' | 'unknown';
134
+ } = {
135
+ message: error.message,
136
+ stack: error.stack,
137
+ errorType: 'unknown',
138
+ };
139
+
140
+ // Check for ToolCallMcpError (MCP protocol-level error with stderr)
141
+ if (error instanceof ToolCallMcpError) {
142
+ console.log(`[API] Detected ToolCallMcpError - stderr:`, error.stderr);
143
+ errorData.errorType = 'mcp';
144
+ errorData.code = error.code;
145
+ errorData.data = error.data;
146
+ errorData.stderr = error.stderr;
147
+ }
148
+ // Check for ToolCallError (tool returned error response)
149
+ else if (error instanceof ToolCallError) {
150
+ console.log(`[API] Detected ToolCallError - result:`, error.result);
151
+ errorData.errorType = 'tool';
152
+ errorData.result = error.result;
153
+ }
154
+ // Fallback: check if it's a generic McpError (has code property)
155
+ else if (error instanceof Error && 'code' in error && typeof (error as { code: unknown }).code === 'number') {
156
+ console.log(`[API] Detected generic McpError-like error`);
157
+ errorData.errorType = 'mcp';
158
+ errorData.code = (error as { code: number }).code;
159
+ if ('data' in error) {
160
+ errorData.data = (error as { data: unknown }).data;
161
+ }
162
+ }
163
+
164
+ console.log(`[API] Final errorData being sent:`, JSON.stringify(errorData, null, 2));
165
+
96
166
  sendExecutionEvent(execId, 'nodeError', {
97
167
  nodeId,
98
168
  nodeType: node.type,
99
169
  executionIndex,
100
170
  input: context, // Include context as input (mcpGraph 0.1.19+ provides actual context)
101
- error: {
102
- message: error.message,
103
- stack: error.stack,
104
- },
171
+ error: errorData,
105
172
  timestamp: Date.now(),
106
173
  });
107
174
  },
108
175
  onPause: async (executionIndex, nodeId, context) => {
109
176
  console.log(`[API] onPause hook called for node: ${nodeId}, executionIndex: ${executionIndex}`);
110
177
 
111
- // Look up node type from config
112
- const api = getApi();
113
- const config = api.getConfig();
114
- const node = config.nodes.find(n => n.id === nodeId);
115
- const nodeType = node?.type || 'unknown';
178
+ // Look up node type from the tool
179
+ const tool = api.getTool(params.toolName);
180
+ let nodeType = 'unknown';
181
+ if (tool && 'nodes' in tool && Array.isArray(tool.nodes)) {
182
+ const node = tool.nodes.find((n: NodeDefinition) => n.id === nodeId);
183
+ nodeType = node?.type || 'unknown';
184
+ }
116
185
 
117
186
  sendExecutionEvent(execId, 'pause', {
118
187
  nodeId,
package/app/page.tsx CHANGED
@@ -17,7 +17,7 @@ interface Tool {
17
17
  export default function Home() {
18
18
  const [tools, setTools] = useState<Tool[]>([]);
19
19
  const [selectedTool, setSelectedTool] = useState<string | null>(null);
20
- const [graphData, setGraphData] = useState<{ nodes: any[]; edges: any[] } | null>(null);
20
+ const [graphData, setGraphData] = useState<{ nodes: Array<{ id: string; type: string; data: Record<string, unknown>; position: { x: number; y: number } }>; edges: Array<{ id: string; source: string; target: string; label?: string }> } | null>(null);
21
21
  const [loading, setLoading] = useState(true);
22
22
  const [error, setError] = useState<string | null>(null);
23
23
  const [serverDetails, setServerDetails] = useState<any>(null);
@@ -25,25 +25,15 @@ export default function Home() {
25
25
  const toolTesterFormSubmitRef = useRef<((formData: Record<string, any>, startPaused: boolean) => void) | null>(null);
26
26
 
27
27
  useEffect(() => {
28
- // Load tools and graph data (graph includes config)
29
- Promise.all([
30
- fetch('/api/tools').then(res => res.json()),
31
- fetch('/api/graph').then(res => res.json()),
32
- ])
33
- .then(([toolsRes, graphRes]) => {
28
+ // Load tools and server config
29
+ fetch('/api/tools')
30
+ .then(res => res.json())
31
+ .then(toolsRes => {
34
32
  if (toolsRes.error) {
35
33
  setError(toolsRes.error);
36
34
  return;
37
35
  }
38
- if (graphRes.error) {
39
- setError(graphRes.error);
40
- return;
41
- }
42
36
  setTools(toolsRes.tools);
43
- setGraphData({ nodes: graphRes.nodes, edges: graphRes.edges });
44
- if (graphRes.config) {
45
- setServerDetails(graphRes.config);
46
- }
47
37
  if (toolsRes.tools.length > 0) {
48
38
  setSelectedTool(toolsRes.tools[0].name);
49
39
  }
@@ -56,6 +46,27 @@ export default function Home() {
56
46
  });
57
47
  }, []);
58
48
 
49
+ // Load graph data and server config when tool is selected
50
+ useEffect(() => {
51
+ if (!selectedTool) return;
52
+
53
+ fetch(`/api/graph?toolName=${encodeURIComponent(selectedTool)}`)
54
+ .then(res => res.json())
55
+ .then(graphRes => {
56
+ if (graphRes.error) {
57
+ setError(graphRes.error);
58
+ return;
59
+ }
60
+ setGraphData({ nodes: graphRes.nodes, edges: graphRes.edges });
61
+ if (graphRes.config) {
62
+ setServerDetails(graphRes.config);
63
+ }
64
+ })
65
+ .catch(err => {
66
+ setError(err.message);
67
+ });
68
+ }, [selectedTool]);
69
+
59
70
  if (loading) {
60
71
  return (
61
72
  <div className={styles.container}>
@@ -138,11 +138,62 @@
138
138
  word-break: break-word;
139
139
  }
140
140
 
141
- .stackTrace {
142
- margin-top: 8px;
141
+
142
+ .errorTypeBadge {
143
+ display: inline-block;
144
+ margin-bottom: 8px;
145
+ padding: 4px 8px;
146
+ background: rgba(198, 40, 40, 0.1);
147
+ border: 1px solid rgba(198, 40, 40, 0.3);
148
+ border-radius: 4px;
149
+ font-size: 10px;
150
+ font-weight: 600;
151
+ color: #c62828;
152
+ }
153
+
154
+ .errorSection {
155
+ margin-top: 12px;
156
+ padding-top: 12px;
157
+ border-top: 1px solid rgba(198, 40, 40, 0.3);
158
+ }
159
+
160
+ .errorSection strong {
161
+ display: block;
162
+ margin-bottom: 4px;
163
+ color: #c62828;
143
164
  font-size: 11px;
144
- color: #999;
165
+ }
166
+
167
+ .errorDataPre {
168
+ margin: 0;
169
+ padding: 8px;
170
+ background: rgba(198, 40, 40, 0.1);
171
+ border: 1px solid rgba(198, 40, 40, 0.3);
172
+ border-radius: 4px;
173
+ font-size: 10px;
174
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
175
+ overflow-x: auto;
176
+ max-height: 200px;
177
+ overflow-y: auto;
178
+ white-space: pre-wrap;
179
+ word-break: break-word;
180
+ color: #c62828;
181
+ }
182
+
183
+ .stderrOutput {
184
+ margin: 0;
185
+ padding: 8px;
186
+ background: rgba(255, 152, 0, 0.1);
187
+ border: 1px solid rgba(255, 152, 0, 0.3);
188
+ border-radius: 4px;
189
+ font-size: 10px;
145
190
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
191
+ overflow-x: auto;
192
+ max-height: 200px;
193
+ overflow-y: auto;
194
+ white-space: pre-wrap;
195
+ word-break: break-word;
196
+ color: #e65100;
146
197
  }
147
198
 
148
199
  .dataSection {
@@ -180,6 +231,22 @@
180
231
  word-break: break-word;
181
232
  }
182
233
 
234
+ .errorOutput {
235
+ margin: 0;
236
+ padding: 8px;
237
+ background: #ffebee;
238
+ border: 1px solid #ef5350;
239
+ border-radius: 4px;
240
+ font-size: 11px;
241
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
242
+ overflow-x: auto;
243
+ max-height: 200px;
244
+ overflow-y: auto;
245
+ white-space: pre-wrap;
246
+ word-break: break-word;
247
+ color: #c62828;
248
+ }
249
+
183
250
  .metadata {
184
251
  display: flex;
185
252
  gap: 16px;
@@ -200,6 +267,15 @@
200
267
  margin-top: auto;
201
268
  }
202
269
 
270
+ .errorResultItem {
271
+ border-top: 3px solid #ef5350;
272
+ background: linear-gradient(to bottom, #ffebee 0%, #ffffff 100%);
273
+ }
274
+
275
+ .errorResultItem .resultHeader {
276
+ border-bottom: 2px solid #ef5350;
277
+ }
278
+
203
279
  .resultHeader {
204
280
  display: flex;
205
281
  justify-content: space-between;
@@ -224,6 +300,12 @@
224
300
  font-weight: bold;
225
301
  }
226
302
 
303
+ .errorIcon {
304
+ font-size: 18px;
305
+ color: #ef5350;
306
+ font-weight: bold;
307
+ }
308
+
227
309
  .resultStats {
228
310
  display: flex;
229
311
  gap: 16px;
@@ -266,3 +348,24 @@
266
348
  color: #333;
267
349
  }
268
350
 
351
+ .errorResult {
352
+ padding: 16px;
353
+ background: white;
354
+ }
355
+
356
+ .errorPre {
357
+ margin: 0;
358
+ padding: 12px;
359
+ background: #ffebee;
360
+ border: 1px solid #ef5350;
361
+ border-radius: 4px;
362
+ font-size: 12px;
363
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
364
+ overflow-x: auto;
365
+ max-height: 300px;
366
+ overflow-y: auto;
367
+ white-space: pre-wrap;
368
+ word-break: break-word;
369
+ color: #c62828;
370
+ }
371
+
@@ -110,32 +110,88 @@ export default function ExecutionHistory({ history, onNodeClick, result, telemet
110
110
 
111
111
  {isExpanded && (
112
112
  <div className={styles.details}>
113
- {hasError && (
114
- <div className={styles.errorSection}>
115
- <strong>Error:</strong>
116
- <pre className={styles.errorMessage}>
117
- {record.error?.message || 'Unknown error'}
118
- {record.error?.stack && (
119
- <div className={styles.stackTrace}>
120
- {record.error.stack}
121
- </div>
122
- )}
123
- </pre>
124
- </div>
125
- )}
126
-
127
113
  <div className={styles.dataSection}>
128
114
  <div className={styles.dataItem}>
129
115
  <strong>Input:</strong>
130
116
  <pre className={styles.jsonData}>{formatJSON(record.input)}</pre>
131
117
  </div>
132
- {!hasError && record.output !== undefined && (
118
+ {hasError ? (
119
+ <div className={styles.dataItem}>
120
+ <strong>Error:</strong>
121
+ <pre className={styles.errorOutput}>
122
+ {(() => {
123
+ const err = record.error;
124
+ if (!err) return 'Unknown error';
125
+
126
+ // Extract error properties
127
+ const errorCode = 'code' in err && typeof err.code === 'number' ? err.code : null;
128
+ const errorData = 'data' in err ? err.data : null;
129
+ const errorType = 'errorType' in err && typeof err.errorType === 'string' ? err.errorType : 'unknown';
130
+ const stderr = 'stderr' in err && Array.isArray(err.stderr) ? err.stderr : null;
131
+ const result = 'result' in err ? err.result : null;
132
+
133
+ // Clean the message - remove stderr part if stderr is available as separate property
134
+ let errorText = err.message || 'Unknown error';
135
+ if (stderr && stderr.length > 0) {
136
+ // Remove the stderr part that was concatenated into the message
137
+ errorText = errorText.split('\n\nServer stderr output:')[0].trim();
138
+ }
139
+
140
+ // Prepend error code if available
141
+ if (errorCode !== null) {
142
+ errorText = `[Error ${errorCode}] ${errorText}`;
143
+ }
144
+
145
+ return (
146
+ <>
147
+ <div className={styles.errorTypeBadge}>
148
+ {errorType === 'mcp' && '🔴 MCP Protocol Error'}
149
+ {errorType === 'tool' && '🟡 Tool Error'}
150
+ {errorType === 'unknown' && '❌ Error'}
151
+ </div>
152
+ {errorText}
153
+
154
+ {/* Show stderr for MCP errors */}
155
+ {stderr && stderr.length > 0 && (
156
+ <div className={styles.errorSection}>
157
+ <strong>Server stderr output:</strong>
158
+ <pre className={styles.stderrOutput}>{stderr.join('\n')}</pre>
159
+ </div>
160
+ )}
161
+
162
+ {/* Show error data for MCP errors */}
163
+ {errorData !== null && errorData !== undefined && (
164
+ <div className={styles.errorSection}>
165
+ <strong>Error Details:</strong>
166
+ <pre className={styles.errorDataPre}>{formatJSON(errorData)}</pre>
167
+ </div>
168
+ )}
169
+
170
+ {/* Show result for tool errors */}
171
+ {result !== null && result !== undefined && (
172
+ <div className={styles.errorSection}>
173
+ <strong>Tool Call Result:</strong>
174
+ <pre className={styles.errorDataPre}>{formatJSON(result)}</pre>
175
+ </div>
176
+ )}
177
+
178
+ {err.stack && (
179
+ <div className={styles.errorSection}>
180
+ <strong>Stack Trace:</strong>
181
+ <pre className={styles.errorDataPre}>{err.stack}</pre>
182
+ </div>
183
+ )}
184
+ </>
185
+ );
186
+ })()}
187
+ </pre>
188
+ </div>
189
+ ) : record.output !== undefined ? (
133
190
  <div className={styles.dataItem}>
134
191
  <strong>Output:</strong>
135
192
  <pre className={styles.jsonData}>{formatJSON(record.output)}</pre>
136
193
  </div>
137
- )}
138
- {!hasError && record.output === undefined && (
194
+ ) : (
139
195
  <div className={styles.dataItem}>
140
196
  <strong>Output:</strong>
141
197
  <div style={{ fontStyle: 'italic', color: '#666' }}>Pending execution</div>
@@ -164,11 +220,20 @@ export default function ExecutionHistory({ history, onNodeClick, result, telemet
164
220
 
165
221
  {/* Result display at the bottom - always expanded and styled to stand out */}
166
222
  {result !== null && result !== undefined && (
167
- <div className={styles.resultItem}>
223
+ <div className={`${styles.resultItem} ${result && typeof result === 'object' && 'error' in result ? styles.errorResultItem : ''}`}>
168
224
  <div className={styles.resultHeader}>
169
225
  <div className={styles.resultTitle}>
170
- <span className={styles.resultIcon}>✓</span>
171
- <strong>Final Result</strong>
226
+ {result && typeof result === 'object' && 'error' in result ? (
227
+ <>
228
+ <span className={styles.errorIcon}>✗</span>
229
+ <strong style={{ color: '#c62828' }}>Execution Error</strong>
230
+ </>
231
+ ) : (
232
+ <>
233
+ <span className={styles.resultIcon}>✓</span>
234
+ <strong>Final Result</strong>
235
+ </>
236
+ )}
172
237
  </div>
173
238
  {telemetry && (
174
239
  <div className={styles.resultStats}>
@@ -187,7 +252,17 @@ export default function ExecutionHistory({ history, onNodeClick, result, telemet
187
252
  )}
188
253
  </div>
189
254
  <div className={styles.resultContent}>
190
- <pre className={styles.resultPre}>{formatJSON(result)}</pre>
255
+ {result && typeof result === 'object' && 'error' in result ? (
256
+ <div className={styles.errorResult}>
257
+ <pre className={styles.errorPre}>
258
+ {typeof result.error === 'string'
259
+ ? result.error
260
+ : 'Execution failed - see node errors above for details'}
261
+ </pre>
262
+ </div>
263
+ ) : (
264
+ <pre className={styles.resultPre}>{formatJSON(result)}</pre>
265
+ )}
191
266
  </div>
192
267
  </div>
193
268
  )}
@@ -37,6 +37,13 @@ interface NodeData {
37
37
  args?: Record<string, unknown>;
38
38
  transform?: { expr: string };
39
39
  conditions?: Array<{ rule?: unknown; target: string }>;
40
+ executionState?: NodeExecutionState;
41
+ isHighlighted?: boolean;
42
+ isCurrentNode?: boolean;
43
+ hasBreakpoint?: boolean;
44
+ onToggleBreakpoint?: (nodeId: string) => void;
45
+ onNodeClick?: (nodeId: string) => void;
46
+ nodeId: string;
40
47
  [key: string]: unknown;
41
48
  }
42
49
 
@@ -232,15 +239,15 @@ function NodeTypeIcon({ nodeType }: { nodeType: string }) {
232
239
  }
233
240
 
234
241
  // Custom node component with top/bottom handles for vertical flow
235
- function CustomNode({ data }: { data: any }) {
242
+ function CustomNode({ data }: { data: NodeData }) {
236
243
  const nodeType = data.nodeType || 'unknown';
237
- const executionState = data.executionState as NodeExecutionState | undefined;
238
- const isHighlighted = data.isHighlighted as boolean | undefined;
239
- const isCurrentNode = data.isCurrentNode as boolean | undefined;
240
- const hasBreakpoint = data.hasBreakpoint as boolean | undefined;
241
- const onToggleBreakpoint = data.onToggleBreakpoint as ((nodeId: string) => void) | undefined;
242
- const onNodeClick = data.onNodeClick as ((nodeId: string) => void) | undefined;
243
- const nodeId = data.nodeId as string;
244
+ const executionState = data.executionState;
245
+ const isHighlighted = data.isHighlighted;
246
+ const isCurrentNode = data.isCurrentNode;
247
+ const hasBreakpoint = data.hasBreakpoint;
248
+ const onToggleBreakpoint = data.onToggleBreakpoint;
249
+ const onNodeClick = data.onNodeClick;
250
+ const nodeId = data.nodeId;
244
251
 
245
252
  const baseStyle = getNodeStyle(nodeType, executionState);
246
253
 
@@ -284,7 +291,7 @@ function CustomNode({ data }: { data: any }) {
284
291
 
285
292
  const handleNodeClick = (e: React.MouseEvent) => {
286
293
  // Don't trigger node click if clicking on breakpoint
287
- if ((e.target as HTMLElement).closest('button')) {
294
+ if (e.target instanceof HTMLElement && e.target.closest('button')) {
288
295
  return;
289
296
  }
290
297
  if (onNodeClick && nodeId) {
@@ -309,7 +316,7 @@ function CustomNode({ data }: { data: any }) {
309
316
  <span>{data.label}</span>
310
317
 
311
318
  {/* Duration */}
312
- {data.duration !== undefined && (
319
+ {typeof data.duration === 'number' && (
313
320
  <span style={{ fontSize: '10px', opacity: 0.7 }}>
314
321
  ({data.duration}ms)
315
322
  </span>
@@ -526,63 +533,10 @@ export default function GraphVisualization({
526
533
  );
527
534
  }, [nodes, edges, executionState, highlightedNode, breakpoints, onToggleBreakpoint, onNodeClick, currentNodeId, setNodes, setEdges]);
528
535
 
529
- // Filter nodes/edges for selected tool if provided
530
- const filteredNodes = useMemo(() => {
531
- if (!selectedTool) return flowNodes;
532
- return flowNodes.filter(node => {
533
- const data = node.data as NodeData;
534
- return (
535
- (data.nodeType === 'entry' && data.tool === selectedTool) ||
536
- (data.nodeType === 'exit' && data.tool === selectedTool) ||
537
- flowEdges.some(edge => {
538
- // Include nodes that are reachable from entry or lead to exit
539
- const entryNode = flowNodes.find(
540
- n => {
541
- const nData = n.data as NodeData;
542
- return nData?.nodeType === 'entry' && nData?.tool === selectedTool;
543
- }
544
- );
545
- const exitNode = flowNodes.find(
546
- n => {
547
- const nData = n.data as NodeData;
548
- return nData?.nodeType === 'exit' && nData?.tool === selectedTool;
549
- }
550
- );
551
-
552
- if (!entryNode || !exitNode) return false;
553
-
554
- // Simple reachability check
555
- const visited = new Set<string>();
556
- const queue = [entryNode.id];
557
- visited.add(entryNode.id);
558
-
559
- while (queue.length > 0) {
560
- const current = queue.shift()!;
561
- if (current === node.id) return true;
562
-
563
- flowEdges
564
- .filter(e => e.source === current)
565
- .forEach(e => {
566
- if (!visited.has(e.target)) {
567
- visited.add(e.target);
568
- queue.push(e.target);
569
- }
570
- });
571
- }
572
-
573
- return false;
574
- })
575
- );
576
- });
577
- }, [flowNodes, flowEdges, selectedTool]);
578
-
579
- const filteredEdges = useMemo(() => {
580
- if (!selectedTool) return flowEdges;
581
- const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
582
- return flowEdges.filter(
583
- edge => filteredNodeIds.has(edge.source) && filteredNodeIds.has(edge.target)
584
- );
585
- }, [flowEdges, filteredNodes, selectedTool]);
536
+ // No need to filter - the API already returns nodes for the selected tool
537
+ // The selectedTool prop is kept for potential future use but filtering is done server-side
538
+ const filteredNodes = flowNodes;
539
+ const filteredEdges = flowEdges;
586
540
 
587
541
  return (
588
542
  <div className={styles.container}>
@@ -111,5 +111,36 @@
111
111
  color: #c62828;
112
112
  border-radius: 4px;
113
113
  border: 1px solid #ffcdd2;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: space-between;
117
+ gap: 12px;
114
118
  }
115
119
 
120
+ .dismissButton {
121
+ background: none;
122
+ border: none;
123
+ color: #c62828;
124
+ font-size: 1.5rem;
125
+ line-height: 1;
126
+ cursor: pointer;
127
+ padding: 0;
128
+ width: 24px;
129
+ height: 24px;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ border-radius: 4px;
134
+ flex-shrink: 0;
135
+ }
136
+
137
+ .dismissButton:hover {
138
+ background-color: rgba(198, 40, 40, 0.1);
139
+ }
140
+
141
+ .dismissButton:focus {
142
+ outline: 2px solid #c62828;
143
+ outline-offset: 2px;
144
+ }
145
+
146
+
@@ -3,17 +3,23 @@
3
3
  import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
4
4
  import styles from './InputForm.module.css';
5
5
 
6
+ interface JsonSchemaProperty {
7
+ type: string;
8
+ description?: string;
9
+ format?: string;
10
+ }
11
+
6
12
  interface ToolInfo {
7
13
  name: string;
8
14
  description: string;
9
15
  inputSchema: {
10
16
  type: string;
11
- properties?: Record<string, any>;
17
+ properties?: Record<string, JsonSchemaProperty>;
12
18
  required?: string[];
13
19
  };
14
20
  outputSchema?: {
15
21
  type: string;
16
- properties?: Record<string, any>;
22
+ properties?: Record<string, JsonSchemaProperty>;
17
23
  };
18
24
  }
19
25
 
@@ -80,7 +86,7 @@ const InputForm = forwardRef<InputFormHandle, InputFormProps>(({ toolName, onSub
80
86
  },
81
87
  }));
82
88
 
83
- const handleInputChange = (key: string, value: any) => {
89
+ const handleInputChange = (key: string, value: unknown) => {
84
90
  setFormData(prev => ({
85
91
  ...prev,
86
92
  [key]: value,
@@ -106,7 +112,7 @@ const InputForm = forwardRef<InputFormHandle, InputFormProps>(({ toolName, onSub
106
112
  onSubmit(formData, startPaused);
107
113
  };
108
114
 
109
- const renderInputField = (key: string, prop: any) => {
115
+ const renderInputField = (key: string, prop: JsonSchemaProperty) => {
110
116
  const value = formData[key];
111
117
  const isRequired = toolInfo?.inputSchema.required?.includes(key);
112
118
 
@@ -245,16 +251,21 @@ const InputForm = forwardRef<InputFormHandle, InputFormProps>(({ toolName, onSub
245
251
  return <div className={styles.loading}>Loading tool information...</div>;
246
252
  }
247
253
 
248
- if (error) {
249
- return (
250
- <div className={styles.error}>
251
- <strong>Error:</strong> {error}
252
- </div>
253
- );
254
- }
255
-
256
254
  return (
257
255
  <form ref={formRef} onSubmit={handleSubmit} className={styles.form}>
256
+ {error && (
257
+ <div className={styles.error}>
258
+ <strong>Error:</strong> {error}
259
+ <button
260
+ type="button"
261
+ onClick={() => setError(null)}
262
+ className={styles.dismissButton}
263
+ aria-label="Dismiss error"
264
+ >
265
+ ×
266
+ </button>
267
+ </div>
268
+ )}
258
269
  <div className={styles.inputs}>
259
270
  {toolInfo.inputSchema.properties &&
260
271
  Object.entries(toolInfo.inputSchema.properties).map(([key, prop]) =>
@@ -45,3 +45,42 @@
45
45
  color: #c62828;
46
46
  }
47
47
 
48
+ .errorBanner {
49
+ padding: 0.75rem 1rem;
50
+ background-color: #ffebee;
51
+ border: 1px solid #ef5350;
52
+ border-radius: 4px;
53
+ color: #c62828;
54
+ margin-bottom: 1rem;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ gap: 12px;
59
+ }
60
+
61
+ .dismissButton {
62
+ background: none;
63
+ border: none;
64
+ color: #c62828;
65
+ font-size: 1.5rem;
66
+ line-height: 1;
67
+ cursor: pointer;
68
+ padding: 0;
69
+ width: 24px;
70
+ height: 24px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ border-radius: 4px;
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .dismissButton:hover {
79
+ background-color: rgba(198, 40, 40, 0.1);
80
+ }
81
+
82
+ .dismissButton:focus {
83
+ outline: 2px solid #c62828;
84
+ outline-offset: 2px;
85
+ }
86
+
@@ -27,6 +27,11 @@ interface NodeErrorEventData {
27
27
  error: {
28
28
  message: string;
29
29
  stack?: string;
30
+ code?: number; // MCP error code (e.g., -32000, -32603)
31
+ data?: unknown; // MCP error data/details
32
+ stderr?: string[]; // For ToolCallMcpError - server stderr output
33
+ result?: unknown; // For ToolCallError - tool call result with error
34
+ errorType: 'mcp' | 'tool' | 'unknown'; // Type of error
30
35
  };
31
36
  timestamp: number;
32
37
  }
@@ -59,7 +64,7 @@ export type { ExecutionStatus };
59
64
 
60
65
  interface ToolTesterProps {
61
66
  toolName: string;
62
- graphData: { nodes: any[]; edges: any[] } | null;
67
+ graphData: { nodes: Array<{ id: string; type: string; data: Record<string, unknown>; position: { x: number; y: number } }>; edges: Array<{ id: string; source: string; target: string; label?: string }> } | null;
63
68
  inputFormRef: React.RefObject<{ submit: (startPaused: boolean) => void }>;
64
69
  onFormSubmit: (handler: (formData: Record<string, any>, startPaused: boolean) => void) => void;
65
70
  }
@@ -75,7 +80,6 @@ export default function ToolTester({
75
80
  // This needs to be after handleSubmit is defined, so we'll do it in a useEffect
76
81
  const formSubmitHandlerRef = useRef<((formData: Record<string, any>, startPaused: boolean) => void) | null>(null);
77
82
  const [loading, setLoading] = useState(false);
78
- const [error, setError] = useState<string | null>(null);
79
83
  const [executionHistory, setExecutionHistory] = useState<NodeExecutionRecord[]>([]);
80
84
  const [telemetry, setTelemetry] = useState<ExecutionTelemetry | null>(null);
81
85
  const [executionStatus, setExecutionStatus] = useState<ExecutionStatus>('not_started');
@@ -127,7 +131,7 @@ export default function ToolTester({
127
131
  setExecutionHistory(prev => {
128
132
  const newHistory = prev.map(record => {
129
133
  // Match by executionIndex if available (from final history)
130
- const recordWithIndex = record as NodeExecutionRecord & { executionIndex?: number };
134
+ const recordWithIndex: NodeExecutionRecord & { executionIndex?: number } = record;
131
135
  if (recordWithIndex.executionIndex === executionIndex) {
132
136
  console.log(`[ToolTester] Updating input for record with executionIndex=${executionIndex}`);
133
137
  return { ...record, input: data.context };
@@ -163,7 +167,7 @@ export default function ToolTester({
163
167
  if (data.history && Array.isArray(data.history)) {
164
168
  // Find the most recent record for this nodeId
165
169
  // The history is in execution order, so the last matching record is the most recent
166
- const records = data.history as Array<{ nodeId: string; executionIndex: number }>;
170
+ const records: Array<{ nodeId: string; executionIndex: number }> = Array.isArray(data.history) ? data.history : [];
167
171
  let matchingRecord: { nodeId: string; executionIndex: number } | undefined;
168
172
 
169
173
  // Find the last (most recent) record for this nodeId
@@ -191,7 +195,6 @@ export default function ToolTester({
191
195
  const currentBreakpoints = breakpointsRef.current;
192
196
 
193
197
  setLoading(true);
194
- setError(null);
195
198
  setExecutionResult(null);
196
199
  setExecutionHistory([]);
197
200
  setTelemetry(null);
@@ -229,6 +232,10 @@ export default function ToolTester({
229
232
  break;
230
233
  case 'nodeStart': {
231
234
  const nodeStartData = event.data as NodeStartEventData;
235
+ if (!nodeStartData || typeof nodeStartData !== 'object' || !('nodeId' in nodeStartData)) {
236
+ console.warn('[ToolTester] Invalid nodeStart event data:', event.data);
237
+ break;
238
+ }
232
239
  const existing = state.get(nodeStartData.nodeId);
233
240
  state.set(nodeStartData.nodeId, {
234
241
  nodeId: nodeStartData.nodeId,
@@ -278,17 +285,21 @@ export default function ToolTester({
278
285
  break;
279
286
  }
280
287
  case 'nodeComplete': {
281
- const existing = state.get(event.data.nodeId);
282
- state.set(event.data.nodeId, {
283
- nodeId: event.data.nodeId,
288
+ const eventData = event.data as NodeCompleteEventData;
289
+ if (!eventData || typeof eventData !== 'object' || !('nodeId' in eventData)) {
290
+ console.warn('[ToolTester] Invalid nodeComplete event data:', event.data);
291
+ break;
292
+ }
293
+ const existing = state.get(eventData.nodeId);
294
+ state.set(eventData.nodeId, {
295
+ nodeId: eventData.nodeId,
284
296
  state: 'completed',
285
- startTime: existing?.startTime || event.data.timestamp,
286
- endTime: event.data.timestamp,
287
- duration: event.data.duration,
297
+ startTime: existing?.startTime || eventData.timestamp,
298
+ endTime: eventData.timestamp,
299
+ duration: eventData.duration,
288
300
  });
289
301
  // Build history record immediately for progressive display
290
302
  // Use input from the event if available, otherwise fetch it
291
- const eventData = event.data as NodeCompleteEventData;
292
303
  const startTime = existing?.startTime || eventData.timestamp;
293
304
  const executionIndex = eventData.executionIndex;
294
305
  const inputFromEvent = eventData.input;
@@ -336,37 +347,89 @@ export default function ToolTester({
336
347
  break;
337
348
  }
338
349
  case 'nodeError': {
339
- const existingError = state.get(event.data.nodeId);
340
- state.set(event.data.nodeId, {
341
- nodeId: event.data.nodeId,
350
+ const eventData = event.data as NodeErrorEventData;
351
+ if (!eventData || typeof eventData !== 'object' || !('nodeId' in eventData)) {
352
+ console.warn('[ToolTester] Invalid nodeError event data:', event.data);
353
+ break;
354
+ }
355
+ const existingError = state.get(eventData.nodeId);
356
+ state.set(eventData.nodeId, {
357
+ nodeId: eventData.nodeId,
342
358
  state: 'error',
343
- startTime: existingError?.startTime || event.data.timestamp,
344
- endTime: event.data.timestamp,
345
- error: event.data.error?.message || 'Unknown error',
359
+ startTime: existingError?.startTime || eventData.timestamp,
360
+ endTime: eventData.timestamp,
361
+ error: eventData.error?.message || 'Unknown error',
346
362
  });
347
363
  // Build history record immediately for progressive display
348
364
  // Use input from the event (mcpGraph 0.1.19+ provides actual context)
349
- const eventData = event.data as NodeErrorEventData;
350
365
  const errorStartTime = existingError?.startTime || eventData.timestamp;
351
366
  const executionIndex = eventData.executionIndex;
352
367
  const inputFromEvent = eventData.input; // Context is now always provided
353
368
 
354
369
  setExecutionHistory(prev => {
355
- const newHistory = [...prev, {
356
- nodeId: eventData.nodeId,
357
- nodeType: eventData.nodeType,
358
- startTime: errorStartTime,
359
- endTime: eventData.timestamp,
360
- duration: eventData.timestamp - errorStartTime,
361
- input: inputFromEvent, // Use input from event
362
- output: undefined,
363
- error: {
364
- message: eventData.error.message,
365
- stack: eventData.error.stack,
366
- } as Error,
367
- executionIndex,
368
- }];
369
- return newHistory;
370
+ // Check if we already have a record for this node (created from pause/nodeStart)
371
+ const existingIndex = prev.findIndex(
372
+ r => r.nodeId === eventData.nodeId && r.executionIndex === executionIndex
373
+ );
374
+
375
+ if (existingIndex >= 0) {
376
+ // Update existing record (created from pause/nodeStart)
377
+ const updated = [...prev];
378
+ updated[existingIndex] = {
379
+ ...updated[existingIndex],
380
+ nodeType: eventData.nodeType,
381
+ startTime: errorStartTime,
382
+ endTime: eventData.timestamp,
383
+ duration: eventData.timestamp - errorStartTime,
384
+ input: inputFromEvent, // Use input from event
385
+ output: undefined,
386
+ error: {
387
+ message: eventData.error.message,
388
+ stack: eventData.error.stack,
389
+ ...(eventData.error.code !== undefined && { code: eventData.error.code }),
390
+ ...(eventData.error.data !== undefined && { data: eventData.error.data }),
391
+ ...(eventData.error.stderr !== undefined && { stderr: eventData.error.stderr }),
392
+ ...(eventData.error.result !== undefined && { result: eventData.error.result }),
393
+ ...(eventData.error.errorType !== undefined && { errorType: eventData.error.errorType }),
394
+ } as Error & {
395
+ code?: number;
396
+ data?: unknown;
397
+ stderr?: string[];
398
+ result?: unknown;
399
+ errorType?: 'mcp' | 'tool' | 'unknown';
400
+ },
401
+ executionIndex,
402
+ };
403
+ return updated;
404
+ } else {
405
+ // Create new record
406
+ const newHistory = [...prev, {
407
+ nodeId: eventData.nodeId,
408
+ nodeType: eventData.nodeType,
409
+ startTime: errorStartTime,
410
+ endTime: eventData.timestamp,
411
+ duration: eventData.timestamp - errorStartTime,
412
+ input: inputFromEvent, // Use input from event
413
+ output: undefined,
414
+ error: {
415
+ message: eventData.error.message,
416
+ stack: eventData.error.stack,
417
+ ...(eventData.error.code !== undefined && { code: eventData.error.code }),
418
+ ...(eventData.error.data !== undefined && { data: eventData.error.data }),
419
+ ...(eventData.error.stderr !== undefined && { stderr: eventData.error.stderr }),
420
+ ...(eventData.error.result !== undefined && { result: eventData.error.result }),
421
+ ...(eventData.error.errorType !== undefined && { errorType: eventData.error.errorType }),
422
+ } as Error & {
423
+ code?: number;
424
+ data?: unknown;
425
+ stderr?: string[];
426
+ result?: unknown;
427
+ errorType?: 'mcp' | 'tool' | 'unknown';
428
+ },
429
+ executionIndex,
430
+ }];
431
+ return newHistory;
432
+ }
370
433
  });
371
434
 
372
435
  // If input wasn't in the event, fetch it using executionIndex
@@ -375,17 +438,21 @@ export default function ToolTester({
375
438
  }
376
439
  break;
377
440
  }
378
- case 'executionComplete':
379
- console.log(`[ToolTester] Execution complete, result:`, event.data.result);
380
- setExecutionResult(event.data.result);
441
+ case 'executionComplete': {
442
+ const completeData = event.data as { result?: unknown; executionHistory?: Array<NodeExecutionRecord & { executionIndex?: number }> };
443
+ if (!completeData || typeof completeData !== 'object') {
444
+ console.warn('[ToolTester] Invalid executionComplete event data:', event.data);
445
+ break;
446
+ }
447
+ console.log(`[ToolTester] Execution complete, result:`, completeData.result);
448
+ setExecutionResult(completeData.result);
381
449
  // The execution history from the API should already have input populated
382
450
  // since we fetch it before unregistering the controller
383
- if (event.data.executionHistory) {
384
- const finalHistory = event.data.executionHistory as Array<NodeExecutionRecord & { executionIndex?: number }>;
385
- setExecutionHistory(finalHistory);
451
+ if (Array.isArray(completeData.executionHistory)) {
452
+ setExecutionHistory(completeData.executionHistory);
386
453
  }
387
- if (event.data.telemetry) {
388
- setTelemetry(event.data.telemetry);
454
+ if ('telemetry' in completeData && completeData.telemetry) {
455
+ setTelemetry(completeData.telemetry as ExecutionTelemetry);
389
456
  }
390
457
  setExecutionStatus('finished');
391
458
  setCurrentNodeId(null);
@@ -393,9 +460,21 @@ export default function ToolTester({
393
460
  stream.disconnect();
394
461
  executionStreamRef.current = null;
395
462
  break;
396
- case 'executionError':
397
- console.log(`[ToolTester] Execution error:`, event.data.error);
398
- setError(event.data.error);
463
+ }
464
+ case 'executionError': {
465
+ if (typeof event.data !== 'object' || !event.data) {
466
+ console.warn('[ToolTester] Invalid executionError event data:', event.data);
467
+ break;
468
+ }
469
+ const errorData = event.data as { error?: string | { message?: string } };
470
+ const fullErrorMessage = typeof errorData.error === 'string'
471
+ ? errorData.error
472
+ : (typeof errorData.error === 'object' && errorData.error?.message) || 'Unknown error';
473
+ // Extract just the first line for the summary (before stderr/details)
474
+ const errorSummary = fullErrorMessage.split('\n')[0].split('Server stderr')[0].trim();
475
+ console.log(`[ToolTester] Execution error:`, fullErrorMessage);
476
+ // Set error as the execution result so it shows in history - just the summary
477
+ setExecutionResult({ error: errorSummary });
399
478
  setExecutionStatus('error');
400
479
  setCurrentNodeId(null);
401
480
  setLoading(false);
@@ -403,6 +482,7 @@ export default function ToolTester({
403
482
  executionStreamRef.current = null;
404
483
  setCurrentExecutionId(null);
405
484
  break;
485
+ }
406
486
  case 'executionStopped':
407
487
  console.log(`[ToolTester] Execution stopped by user`);
408
488
  setExecutionStatus('stopped');
@@ -414,6 +494,10 @@ export default function ToolTester({
414
494
  break;
415
495
  case 'pause': {
416
496
  const pauseData = event.data as PauseEventData;
497
+ if (!pauseData || typeof pauseData !== 'object' || !('nodeId' in pauseData)) {
498
+ console.warn('[ToolTester] Invalid pause event data:', event.data);
499
+ break;
500
+ }
417
501
  console.log(`[ToolTester] Pause event received for node: ${pauseData.nodeId}`);
418
502
  setExecutionStatus('paused');
419
503
  if (pauseData.nodeId) {
@@ -457,19 +541,21 @@ export default function ToolTester({
457
541
  // Don't set status here - stateUpdate is the authoritative source
458
542
  // The resume event is just informational, stateUpdate will follow with the actual status
459
543
  break;
460
- case 'stateUpdate':
461
- console.log(`[ToolTester] stateUpdate event:`, event.data);
462
- if (event.data.status) {
463
- console.log(`[ToolTester] Setting execution status to: ${event.data.status}`);
464
- setExecutionStatus(event.data.status);
544
+ case 'stateUpdate': {
545
+ const stateData = event.data as { status?: ExecutionStatus; currentNodeId?: string | null };
546
+ console.log(`[ToolTester] stateUpdate event:`, stateData);
547
+ if (stateData.status) {
548
+ console.log(`[ToolTester] Setting execution status to: ${stateData.status}`);
549
+ setExecutionStatus(stateData.status);
465
550
  }
466
- if (event.data.currentNodeId !== undefined) {
467
- setCurrentNodeId(event.data.currentNodeId);
468
- } else if (event.data.status === 'running' || event.data.status === 'finished' || event.data.status === 'error' || event.data.status === 'stopped') {
551
+ if (stateData.currentNodeId !== undefined) {
552
+ setCurrentNodeId(stateData.currentNodeId);
553
+ } else if (stateData.status === 'running' || stateData.status === 'finished' || stateData.status === 'error' || stateData.status === 'stopped') {
469
554
  // Clear currentNodeId when execution is no longer paused
470
555
  setCurrentNodeId(null);
471
556
  }
472
557
  break;
558
+ }
473
559
  }
474
560
 
475
561
  // Update execution state
@@ -518,7 +604,16 @@ export default function ToolTester({
518
604
  const data = await response.json();
519
605
 
520
606
  if (data.error) {
521
- setError(data.error);
607
+ // Show error in execution history, not as a banner - just the summary
608
+ const fullError = typeof data.error === 'string'
609
+ ? data.error
610
+ : (typeof data.error === 'object' && data.error !== null && 'message' in data.error)
611
+ ? String((data.error as { message: unknown }).message)
612
+ : 'Unknown error';
613
+ // Extract just the first line for the summary (before stderr/details)
614
+ const errorSummary = fullError.split('\n')[0].split('Server stderr')[0].trim();
615
+ setExecutionResult({ error: errorSummary });
616
+ setExecutionStatus('error');
522
617
  setLoading(false);
523
618
  stream.disconnect();
524
619
  executionStreamRef.current = null;
@@ -526,7 +621,9 @@ export default function ToolTester({
526
621
  // Result will be set via SSE executionComplete event
527
622
  } catch (err) {
528
623
  console.error(`[ToolTester] Error executing tool:`, err);
529
- setError(err instanceof Error ? err.message : 'Unknown error');
624
+ // Show error in execution history, not as a banner
625
+ setExecutionResult({ error: err instanceof Error ? err.message : 'Unknown error' });
626
+ setExecutionStatus('error');
530
627
  setLoading(false);
531
628
  stream.disconnect();
532
629
  executionStreamRef.current = null;
@@ -552,7 +649,6 @@ export default function ToolTester({
552
649
  const handleClear = () => {
553
650
  // Reset all execution-related state
554
651
  setLoading(false);
555
- setError(null);
556
652
  setExecutionResult(null);
557
653
  setExecutionHistory([]);
558
654
  setTelemetry(null);
@@ -581,7 +677,32 @@ export default function ToolTester({
581
677
  breakpointsRef.current = breakpoints;
582
678
  }, [breakpoints]);
583
679
 
680
+ // Reset execution state when toolName changes (switching to a different tool)
681
+ useEffect(() => {
682
+ // Clear all execution-related state when tool changes
683
+ setLoading(false);
684
+ setExecutionResult(null);
685
+ setExecutionHistory([]);
686
+ setTelemetry(null);
687
+ setExecutionStatus('not_started');
688
+ setCurrentNodeId(null);
689
+ setCurrentExecutionId(null);
690
+ setExecutionState(new Map());
691
+ setHighlightedNode(null);
692
+ const emptyBreakpoints = new Set<string>();
693
+ setBreakpoints(emptyBreakpoints);
694
+ breakpointsRef.current = emptyBreakpoints;
695
+ executionStateRef.current.clear();
696
+
697
+ // Disconnect any active stream
698
+ if (executionStreamRef.current) {
699
+ executionStreamRef.current.disconnect();
700
+ executionStreamRef.current = null;
701
+ }
702
+ }, [toolName]);
703
+
584
704
  // Expose form submit handler to parent (so InputForm can call it)
705
+ // Update when toolName changes to ensure we use the correct tool
585
706
  useEffect(() => {
586
707
  // Update the ref with the current handleSubmit
587
708
  // The handleSubmit function will read breakpoints from breakpointsRef when called
@@ -599,8 +720,10 @@ export default function ToolTester({
599
720
  } else {
600
721
  console.warn('[ToolTester] onFormSubmit is not provided');
601
722
  }
723
+ // Update handler when toolName changes so it uses the correct tool
724
+ // handleSubmit is recreated on every render but uses toolName from props, so we need to update the ref
602
725
  // eslint-disable-next-line react-hooks/exhaustive-deps
603
- }, []);
726
+ }, [toolName, onFormSubmit]);
604
727
 
605
728
  // Cleanup on unmount
606
729
  useEffect(() => {
@@ -628,16 +751,6 @@ export default function ToolTester({
628
751
  setTimeout(() => setHighlightedNode(null), 2000);
629
752
  };
630
753
 
631
- if (error) {
632
- return (
633
- <div className={styles.container}>
634
- <div className={styles.error}>
635
- <strong>Error:</strong> {error}
636
- </div>
637
- </div>
638
- );
639
- }
640
-
641
754
  return (
642
755
  <div className={styles.container}>
643
756
  <div className={styles.debugControlsHeader}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpgraph-ux",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Visual interface for mcpGraph - visualize and test MCP tool execution graphs",
5
5
  "main": "server.ts",
6
6
  "bin": {
@@ -25,23 +25,23 @@
25
25
  "package.json"
26
26
  ],
27
27
  "dependencies": {
28
+ "dagre": "^0.8.5",
29
+ "mcpgraph": "^0.1.23",
28
30
  "next": "^14.2.0",
29
31
  "react": "^18.3.0",
30
32
  "react-dom": "^18.3.0",
31
33
  "reactflow": "^11.11.0",
32
- "mcpgraph": "^0.1.19",
33
- "zod": "^3.22.4",
34
- "dagre": "^0.8.5",
35
- "tsx": "^4.7.0"
34
+ "tsx": "^4.7.0",
35
+ "zod": "^3.22.4"
36
36
  },
37
37
  "devDependencies": {
38
+ "@types/dagre": "^0.7.52",
38
39
  "@types/node": "^20.10.0",
39
40
  "@types/react": "^18.3.0",
40
41
  "@types/react-dom": "^18.3.0",
41
- "@types/dagre": "^0.7.52",
42
- "typescript": "^5.3.3",
43
42
  "eslint": "^8.57.0",
44
- "eslint-config-next": "^14.2.0"
43
+ "eslint-config-next": "^14.2.0",
44
+ "typescript": "^5.3.3"
45
45
  },
46
46
  "keywords": [
47
47
  "mcp",
@@ -63,4 +63,3 @@
63
63
  "node": ">=20.0.0"
64
64
  }
65
65
  }
66
-