mcpgraph-ux 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,16 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { McpGraphApi } from 'mcpgraph';
2
+ import {
3
+ type ExecutionOptions,
4
+ type ExecutionHooks,
5
+ type ExecutionResult,
6
+ type NodeDefinition,
7
+ ToolCallMcpError,
8
+ ToolCallError
9
+ } from 'mcpgraph';
10
+ import { getApi } from '@/lib/mcpGraphApi';
11
+ import { sendExecutionEvent, closeExecutionStream } from '@/lib/executionStreamServer';
12
+ import { registerController, unregisterController, getController } from '@/lib/executionController';
3
13
 
4
- // Force dynamic rendering - this route requires runtime config
5
- export const dynamic = 'force-dynamic';
6
-
7
- let apiInstance: McpGraphApi | null = null;
8
-
9
- function getApi(): McpGraphApi {
10
- const configPath = process.env.MCPGRAPH_CONFIG_PATH;
11
- if (!configPath) {
12
- throw new Error('MCPGRAPH_CONFIG_PATH environment variable is not set');
13
- }
14
-
15
- if (!apiInstance) {
16
- apiInstance = new McpGraphApi(configPath);
17
- }
18
-
19
- return apiInstance;
20
- }
21
14
 
22
15
  export async function GET(
23
16
  request: Request,
@@ -48,16 +41,340 @@ export async function POST(
48
41
  request: Request,
49
42
  { params }: { params: { toolName: string } }
50
43
  ) {
44
+ let executionId: string | undefined;
51
45
  try {
52
46
  const api = getApi();
53
- const body = await request.json();
47
+ const body = await request.json() as {
48
+ args?: Record<string, unknown>;
49
+ executionId?: string;
50
+ options?: ExecutionOptions;
51
+ };
54
52
  const args = body.args || {};
53
+ executionId = body.executionId;
54
+ const executionOptions = body.options;
55
+
56
+ // Log breakpoints received
57
+ const breakpointsReceived = executionOptions?.breakpoints || [];
58
+ console.log(`[API] Received breakpoints: ${breakpointsReceived.length > 0 ? breakpointsReceived.join(', ') : 'none'}`);
59
+
60
+ // If executionId is provided, set up hooks to stream events via SSE
61
+ // Store breakpoints for use in hooks (controller may not be available yet)
62
+ const breakpointsList = executionOptions?.breakpoints || [];
63
+ let hooks: ExecutionHooks | undefined;
64
+ if (executionId) {
65
+ const execId = executionId; // Capture in const for closure
66
+ console.log(`[API] Setting up hooks for executionId: ${execId}, breakpoints: ${breakpointsList.join(', ')}`);
67
+ hooks = {
68
+ onNodeStart: async (executionIndex, nodeId, node, context) => {
69
+ console.log(`[API] onNodeStart hook called for node: ${nodeId}, executionIndex: ${executionIndex}`);
70
+
71
+ // Check if this node should have a breakpoint
72
+ const controller = getController(execId);
73
+ if (controller && breakpointsList.includes(nodeId)) {
74
+ const controllerBreakpoints = controller.getBreakpoints();
75
+ const state = controller.getState();
76
+ console.log(`[API] WARNING: onNodeStart called for node ${nodeId} which has a breakpoint!`);
77
+ console.log(`[API] Controller breakpoints: ${controllerBreakpoints.length > 0 ? controllerBreakpoints.join(', ') : 'none'}`);
78
+ console.log(`[API] Controller status: ${state.status}, currentNodeId: ${state.currentNodeId}`);
79
+ }
80
+
81
+ sendExecutionEvent(execId, 'nodeStart', {
82
+ nodeId,
83
+ nodeType: node.type,
84
+ executionIndex,
85
+ context, // Send context so client can determine input
86
+ timestamp: Date.now(),
87
+ });
88
+
89
+ // Note: mcpGraph should check breakpoints internally before executing nodes
90
+ // If we reach here, the node is starting. The controller's breakpoint checking
91
+ // should have paused execution before this hook is called.
92
+
93
+ return true; // Continue execution
94
+ },
95
+ onNodeComplete: async (executionIndex, nodeId, node, input, output, duration) => {
96
+ sendExecutionEvent(execId, 'nodeComplete', {
97
+ nodeId,
98
+ nodeType: node.type,
99
+ executionIndex,
100
+ input,
101
+ output,
102
+ duration,
103
+ timestamp: Date.now(),
104
+ });
105
+ },
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
+
166
+ sendExecutionEvent(execId, 'nodeError', {
167
+ nodeId,
168
+ nodeType: node.type,
169
+ executionIndex,
170
+ input: context, // Include context as input (mcpGraph 0.1.19+ provides actual context)
171
+ error: errorData,
172
+ timestamp: Date.now(),
173
+ });
174
+ },
175
+ onPause: async (executionIndex, nodeId, context) => {
176
+ console.log(`[API] onPause hook called for node: ${nodeId}, executionIndex: ${executionIndex}`);
177
+
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
+ }
185
+
186
+ sendExecutionEvent(execId, 'pause', {
187
+ nodeId,
188
+ nodeType,
189
+ executionIndex,
190
+ context, // Include context so client can show input for pending node
191
+ timestamp: Date.now(),
192
+ });
193
+ // Send stateUpdate to ensure UI status is updated
194
+ sendExecutionEvent(execId, 'stateUpdate', {
195
+ status: 'paused',
196
+ currentNodeId: nodeId,
197
+ timestamp: Date.now(),
198
+ });
199
+ },
200
+ onResume: async () => {
201
+ sendExecutionEvent(execId, 'resume', {
202
+ timestamp: Date.now(),
203
+ });
204
+ },
205
+ };
206
+ }
207
+
208
+ // Merge with provided hooks if any
209
+ const finalOptions: ExecutionOptions = {
210
+ ...executionOptions,
211
+ hooks: executionOptions?.hooks
212
+ ? {
213
+ ...hooks,
214
+ ...executionOptions.hooks,
215
+ // Merge hook functions - call both
216
+ onNodeStart: async (executionIndex, nodeId, node, context) => {
217
+ const hook1 = hooks?.onNodeStart;
218
+ const hook2 = executionOptions.hooks?.onNodeStart;
219
+ const result1 = hook1 ? await hook1(executionIndex, nodeId, node, context) : true;
220
+ const result2 = hook2 ? await hook2(executionIndex, nodeId, node, context) : true;
221
+ return result1 && result2;
222
+ },
223
+ onNodeComplete: async (executionIndex, nodeId, node, input, output, duration) => {
224
+ await hooks?.onNodeComplete?.(executionIndex, nodeId, node, input, output, duration);
225
+ await executionOptions.hooks?.onNodeComplete?.(executionIndex, nodeId, node, input, output, duration);
226
+ },
227
+ onNodeError: async (executionIndex, nodeId, node, error, context) => {
228
+ await hooks?.onNodeError?.(executionIndex, nodeId, node, error, context);
229
+ await executionOptions.hooks?.onNodeError?.(executionIndex, nodeId, node, error, context);
230
+ },
231
+ onPause: async (executionIndex, nodeId, context) => {
232
+ await hooks?.onPause?.(executionIndex, nodeId, context);
233
+ await executionOptions.hooks?.onPause?.(executionIndex, nodeId, context);
234
+ },
235
+ onResume: async () => {
236
+ await hooks?.onResume?.();
237
+ await executionOptions.hooks?.onResume?.();
238
+ },
239
+ }
240
+ : hooks,
241
+ breakpoints: executionOptions?.breakpoints,
242
+ enableTelemetry: executionOptions?.enableTelemetry ?? true, // Enable by default for UX
243
+ };
244
+
245
+ console.log(`[API] Executing tool ${params.toolName} with executionId: ${executionId || 'none'}`);
246
+ const finalBreakpoints = finalOptions.breakpoints || [];
247
+ console.log(`[API] Final options breakpoints: ${finalBreakpoints.length > 0 ? finalBreakpoints.join(', ') : 'none'}`);
248
+ console.log(`[API] Final options object:`, JSON.stringify({
249
+ breakpoints: finalBreakpoints,
250
+ enableTelemetry: finalOptions.enableTelemetry,
251
+ hasHooks: !!finalOptions.hooks
252
+ }, null, 2));
253
+
254
+ // Start execution and get controller directly (mcpGraph 0.1.11+ returns both)
255
+ const { promise: executionPromise, controller } = api.executeTool(params.toolName, args, finalOptions);
256
+
257
+ console.log(`[API] executeTool returned controller: ${controller ? 'present' : 'null'}, executionId: ${executionId || 'none'}`);
258
+
259
+ // Register controller immediately if we have executionId and controller
260
+ if (executionId && controller) {
261
+ const execId = executionId; // Capture in const for closure
262
+ registerController(execId, controller);
263
+ console.log(`[API] Registered controller for executionId: ${execId}`);
264
+
265
+ // Log breakpoints on controller
266
+ const controllerBreakpoints = controller.getBreakpoints();
267
+ console.log(`[API] Controller breakpoints: ${controllerBreakpoints.length > 0 ? controllerBreakpoints.join(', ') : 'none'}`);
268
+ } else {
269
+ if (!executionId) {
270
+ console.log(`[API] WARNING: No executionId provided, controller not registered`);
271
+ }
272
+ if (!controller) {
273
+ console.log(`[API] WARNING: Controller is null, cannot register. Hooks: ${!!finalOptions.hooks}, Breakpoints: ${finalBreakpoints.length > 0 ? finalBreakpoints.join(', ') : 'none'}`);
274
+ }
275
+ }
276
+
277
+ const result = await executionPromise;
278
+ console.log(`[API] Tool execution completed, result:`, result.result ? 'present' : 'missing');
279
+
280
+ // Send completion event and close stream
281
+ if (executionId) {
282
+ const execId = executionId; // Capture in const for closure
283
+ console.log(`[API] Sending executionComplete event for executionId: ${execId}`);
284
+
285
+ // Fetch input contexts for all execution records BEFORE unregistering controller
286
+ // Use the controller's context directly since api.getContextForExecution() requires active controller
287
+ const controller = getController(execId);
288
+ console.log(`[API] Fetching input contexts for ${result.executionHistory?.length || 0} records`);
289
+ const executionHistoryWithInput = (result.executionHistory || []).map((record) => {
290
+ console.log(`[API] Processing record: nodeId=${record.nodeId}, executionIndex=${record.executionIndex}`);
291
+ try {
292
+ let context: Record<string, unknown> | null = null;
293
+ if (controller) {
294
+ const state = controller.getState();
295
+ context = state.context.getContextForExecution(record.executionIndex);
296
+ } else {
297
+ console.warn(`[API] Controller not found for ${execId}, trying API method`);
298
+ context = api.getContextForExecution(record.executionIndex);
299
+ }
300
+ console.log(`[API] Got context for ${record.nodeId}:`, context ? 'present' : 'null', context);
301
+ return {
302
+ ...record,
303
+ input: context || undefined,
304
+ };
305
+ } catch (error) {
306
+ console.error(`[API] Error getting context for executionIndex ${record.executionIndex}:`, error);
307
+ return record;
308
+ }
309
+ });
310
+ console.log(`[API] Final execution history with input:`, JSON.stringify(executionHistoryWithInput, null, 2));
311
+
312
+ sendExecutionEvent(execId, 'executionComplete', {
313
+ result: result.result,
314
+ executionHistory: executionHistoryWithInput,
315
+ telemetry: result.telemetry
316
+ ? {
317
+ ...result.telemetry,
318
+ nodeDurations: Object.fromEntries(result.telemetry.nodeDurations),
319
+ nodeCounts: Object.fromEntries(result.telemetry.nodeCounts),
320
+ }
321
+ : undefined,
322
+ timestamp: Date.now(),
323
+ });
324
+ // Close stream immediately - enqueue is synchronous, event is already sent
325
+ console.log(`[API] Closing stream for executionId: ${execId}`);
326
+ closeExecutionStream(execId);
327
+ unregisterController(execId);
328
+ }
55
329
 
56
- const result = await api.executeTool(params.toolName, args);
330
+ // Serialize telemetry Maps for JSON response
331
+ const responseResult = {
332
+ ...result,
333
+ telemetry: result.telemetry
334
+ ? {
335
+ ...result.telemetry,
336
+ nodeDurations: Object.fromEntries(result.telemetry.nodeDurations),
337
+ nodeCounts: Object.fromEntries(result.telemetry.nodeCounts),
338
+ }
339
+ : undefined,
340
+ };
57
341
 
58
- return NextResponse.json({ result });
342
+ return NextResponse.json({ result: responseResult });
59
343
  } catch (error) {
60
344
  console.error('Error executing tool:', error);
345
+
346
+ // Check if execution was stopped (not a real error)
347
+ const isStopped = error instanceof Error && error.message === 'Execution was stopped';
348
+
349
+ if (executionId) {
350
+ const execId = executionId; // Capture in const for closure
351
+
352
+ if (isStopped) {
353
+ // Execution was stopped by user - send stopped event, not error
354
+ sendExecutionEvent(execId, 'executionStopped', {
355
+ timestamp: Date.now(),
356
+ });
357
+ } else {
358
+ // Real error occurred
359
+ sendExecutionEvent(execId, 'executionError', {
360
+ error: error instanceof Error ? error.message : 'Unknown error',
361
+ timestamp: Date.now(),
362
+ });
363
+ }
364
+
365
+ // Close stream immediately - enqueue is synchronous, event is already sent
366
+ closeExecutionStream(execId);
367
+ unregisterController(execId);
368
+ }
369
+
370
+ // If stopped, return success (stopping is intentional)
371
+ if (isStopped) {
372
+ return NextResponse.json({
373
+ result: null,
374
+ stopped: true
375
+ });
376
+ }
377
+
61
378
  return NextResponse.json(
62
379
  { error: error instanceof Error ? error.message : 'Unknown error' },
63
380
  { status: 500 }
@@ -1,24 +1,9 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { McpGraphApi } from 'mcpgraph';
2
+ import { getApi } from '@/lib/mcpGraphApi';
3
3
 
4
4
  // Force dynamic rendering - this route requires runtime config
5
5
  export const dynamic = 'force-dynamic';
6
6
 
7
- let apiInstance: McpGraphApi | null = null;
8
-
9
- function getApi(): McpGraphApi {
10
- const configPath = process.env.MCPGRAPH_CONFIG_PATH;
11
- if (!configPath) {
12
- throw new Error('MCPGRAPH_CONFIG_PATH environment variable is not set');
13
- }
14
-
15
- if (!apiInstance) {
16
- apiInstance = new McpGraphApi(configPath);
17
- }
18
-
19
- return apiInstance;
20
- }
21
-
22
7
  export async function GET() {
23
8
  try {
24
9
  const api = getApi();
@@ -33,7 +33,9 @@
33
33
  width: 300px;
34
34
  background-color: #fff;
35
35
  border-right: 1px solid #e0e0e0;
36
- overflow-y: auto;
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden;
37
39
  }
38
40
 
39
41
  .content {
@@ -43,35 +45,79 @@
43
45
  overflow: hidden;
44
46
  }
45
47
 
46
- .graphSection {
47
- flex: 1;
48
- display: flex;
49
- flex-direction: column;
48
+ .testerSection {
49
+ background-color: #fff;
50
+ padding: 0.75rem 1.5rem;
50
51
  border-bottom: 1px solid #e0e0e0;
51
- background-color: #fafafa;
52
+ flex-shrink: 0;
52
53
  }
53
54
 
54
- .graphSection h2 {
55
- padding: 1rem 1.5rem;
55
+ .testerSection h2 {
56
56
  font-size: 1.1rem;
57
57
  font-weight: 600;
58
+ margin-bottom: 0.5rem;
58
59
  color: #333;
59
- border-bottom: 1px solid #e0e0e0;
60
+ }
61
+
62
+ .bottomSection {
63
+ flex: 1;
64
+ display: flex;
65
+ flex-direction: column;
66
+ overflow: hidden;
67
+ min-height: 0;
68
+ border: 1px solid #e0e0e0;
69
+ border-radius: 8px;
60
70
  background-color: #fff;
61
71
  }
62
72
 
63
- .testerSection {
73
+ .debugControlsHeader {
74
+ flex-shrink: 0;
75
+ border-bottom: 1px solid #e0e0e0;
76
+ background-color: #fafafa;
77
+ border-radius: 8px 8px 0 0;
78
+ }
79
+
80
+ .graphSection {
81
+ flex: 1;
82
+ display: flex;
83
+ flex-direction: column;
84
+ border-right: 1px solid #e0e0e0;
85
+ background-color: #fafafa;
86
+ overflow: hidden;
87
+ min-width: 0;
88
+ min-height: 0;
89
+ }
90
+
91
+ .graphHistoryContainer {
92
+ flex: 1;
93
+ display: flex;
94
+ overflow: hidden;
95
+ min-height: 0;
96
+ }
97
+
98
+
99
+ .historySection {
100
+ flex: 1;
101
+ display: flex;
102
+ flex-direction: column;
64
103
  background-color: #fff;
65
- padding: 1.5rem;
66
- overflow-y: auto;
67
- max-height: 400px;
104
+ overflow: hidden;
105
+ min-width: 0;
68
106
  }
69
107
 
70
- .testerSection h2 {
71
- font-size: 1.1rem;
72
- font-weight: 600;
73
- margin-bottom: 1rem;
74
- color: #333;
108
+
109
+ .emptyHistory {
110
+ padding: 2rem;
111
+ text-align: center;
112
+ color: #666;
113
+ font-style: italic;
114
+ }
115
+
116
+ .historySection > :global(.container) {
117
+ flex: 1;
118
+ display: flex;
119
+ flex-direction: column;
120
+ min-height: 0;
75
121
  }
76
122
 
77
123
  .loading,
package/app/page.tsx CHANGED
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
4
- import GraphVisualization from '@/components/GraphVisualization';
3
+ import { useEffect, useState, useRef } from 'react';
5
4
  import ToolList from '@/components/ToolList';
6
5
  import ToolTester from '@/components/ToolTester';
6
+ import InputForm, { type InputFormHandle } from '@/components/InputForm';
7
+ import { ServerConfig, McpServers } from '@/components/ServerDetails';
7
8
  import styles from './page.module.css';
8
9
 
9
10
  interface Tool {
@@ -16,27 +17,23 @@ interface Tool {
16
17
  export default function Home() {
17
18
  const [tools, setTools] = useState<Tool[]>([]);
18
19
  const [selectedTool, setSelectedTool] = useState<string | null>(null);
19
- 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);
20
21
  const [loading, setLoading] = useState(true);
21
22
  const [error, setError] = useState<string | null>(null);
23
+ const [serverDetails, setServerDetails] = useState<any>(null);
24
+ const inputFormRef = useRef<InputFormHandle>(null);
25
+ const toolTesterFormSubmitRef = useRef<((formData: Record<string, any>, startPaused: boolean) => void) | null>(null);
22
26
 
23
27
  useEffect(() => {
24
- // Load tools and graph data
25
- Promise.all([
26
- fetch('/api/tools').then(res => res.json()),
27
- fetch('/api/graph').then(res => res.json()),
28
- ])
29
- .then(([toolsRes, graphRes]) => {
28
+ // Load tools and server config
29
+ fetch('/api/tools')
30
+ .then(res => res.json())
31
+ .then(toolsRes => {
30
32
  if (toolsRes.error) {
31
33
  setError(toolsRes.error);
32
34
  return;
33
35
  }
34
- if (graphRes.error) {
35
- setError(graphRes.error);
36
- return;
37
- }
38
36
  setTools(toolsRes.tools);
39
- setGraphData({ nodes: graphRes.nodes, edges: graphRes.edges });
40
37
  if (toolsRes.tools.length > 0) {
41
38
  setSelectedTool(toolsRes.tools[0].name);
42
39
  }
@@ -49,6 +46,27 @@ export default function Home() {
49
46
  });
50
47
  }, []);
51
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
+
52
70
  if (loading) {
53
71
  return (
54
72
  <div className={styles.container}>
@@ -77,31 +95,47 @@ export default function Home() {
77
95
 
78
96
  <div className={styles.main}>
79
97
  <div className={styles.sidebar}>
98
+ {serverDetails && <ServerConfig config={serverDetails} />}
80
99
  <ToolList
81
100
  tools={tools}
82
101
  selectedTool={selectedTool}
83
102
  onSelectTool={setSelectedTool}
84
103
  />
104
+ {serverDetails && <McpServers config={serverDetails} />}
85
105
  </div>
86
106
 
87
107
  <div className={styles.content}>
88
- <div className={styles.graphSection}>
89
- <h2>Graph Visualization</h2>
90
- {graphData && (
91
- <GraphVisualization
92
- nodes={graphData.nodes}
93
- edges={graphData.edges}
94
- selectedTool={selectedTool}
95
- />
96
- )}
97
- </div>
98
-
108
+ {/* Top section: Input form */}
99
109
  {selectedTool && (
100
110
  <div className={styles.testerSection}>
101
111
  <h2>Test Tool: {selectedTool}</h2>
102
- <ToolTester toolName={selectedTool} />
112
+ <InputForm
113
+ ref={inputFormRef}
114
+ toolName={selectedTool}
115
+ onSubmit={(data, startPaused) => {
116
+ // Form validated and collected data - pass to ToolTester for execution
117
+ if (toolTesterFormSubmitRef.current) {
118
+ toolTesterFormSubmitRef.current(data, startPaused);
119
+ } else {
120
+ console.warn('[page.tsx] toolTesterFormSubmitRef.current is null');
121
+ }
122
+ }}
123
+ />
103
124
  </div>
104
125
  )}
126
+
127
+ {/* Bottom section: Execution/testing area (includes debug controls, graph, and history) */}
128
+ {selectedTool && graphData && (
129
+ <ToolTester
130
+ toolName={selectedTool}
131
+ graphData={graphData}
132
+ inputFormRef={inputFormRef}
133
+ onFormSubmit={(handler) => {
134
+ // Store the handler that InputForm will call
135
+ toolTesterFormSubmitRef.current = handler;
136
+ }}
137
+ />
138
+ )}
105
139
  </div>
106
140
  </div>
107
141
  </div>