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 +4 -0
- package/app/api/graph/route.ts +40 -5
- package/app/api/tools/[toolName]/route.ts +82 -13
- package/app/page.tsx +26 -15
- package/components/ExecutionHistory.module.css +106 -3
- package/components/ExecutionHistory.tsx +96 -21
- package/components/GraphVisualization.tsx +21 -67
- package/components/InputForm.module.css +31 -0
- package/components/InputForm.tsx +23 -12
- package/components/ToolTester.module.css +39 -0
- package/components/ToolTester.tsx +180 -67
- package/package.json +8 -9
package/README.md
CHANGED
package/app/api/graph/route.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
43
|
-
const executionOptions = body.options
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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:
|
|
242
|
+
function CustomNode({ data }: { data: NodeData }) {
|
|
236
243
|
const nodeType = data.nodeType || 'unknown';
|
|
237
|
-
const executionState = data.executionState
|
|
238
|
-
const isHighlighted = data.isHighlighted
|
|
239
|
-
const isCurrentNode = data.isCurrentNode
|
|
240
|
-
const hasBreakpoint = data.hasBreakpoint
|
|
241
|
-
const onToggleBreakpoint = data.onToggleBreakpoint
|
|
242
|
-
const onNodeClick = data.onNodeClick
|
|
243
|
-
const nodeId = data.nodeId
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
|
package/components/InputForm.tsx
CHANGED
|
@@ -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,
|
|
17
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
12
18
|
required?: string[];
|
|
13
19
|
};
|
|
14
20
|
outputSchema?: {
|
|
15
21
|
type: string;
|
|
16
|
-
properties?: Record<string,
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
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 ||
|
|
286
|
-
endTime:
|
|
287
|
-
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
|
|
340
|
-
|
|
341
|
-
|
|
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 ||
|
|
344
|
-
endTime:
|
|
345
|
-
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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 (
|
|
384
|
-
|
|
385
|
-
setExecutionHistory(finalHistory);
|
|
451
|
+
if (Array.isArray(completeData.executionHistory)) {
|
|
452
|
+
setExecutionHistory(completeData.executionHistory);
|
|
386
453
|
}
|
|
387
|
-
if (
|
|
388
|
-
setTelemetry(
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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 (
|
|
467
|
-
setCurrentNodeId(
|
|
468
|
-
} else if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
-
"
|
|
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
|
-
|