mcpgraph-ux 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/app/api/execution/breakpoints/route.ts +106 -0
- package/app/api/execution/context/route.ts +69 -0
- package/app/api/execution/controller/route.ts +121 -0
- package/app/api/execution/history/route.ts +40 -0
- package/app/api/execution/history-with-indices/route.ts +57 -0
- package/app/api/execution/stream/route.ts +34 -0
- package/app/api/graph/route.ts +63 -29
- package/app/api/tools/[toolName]/route.ts +268 -17
- package/app/api/tools/route.ts +3 -15
- package/app/page.module.css +64 -18
- package/app/page.tsx +38 -15
- package/components/DebugControls.module.css +124 -0
- package/components/DebugControls.tsx +209 -0
- package/components/ExecutionHistory.module.css +268 -0
- package/components/ExecutionHistory.tsx +197 -0
- package/components/GraphVisualization.module.css +11 -0
- package/components/GraphVisualization.tsx +350 -21
- package/components/InputForm.module.css +115 -0
- package/components/InputForm.tsx +271 -0
- package/components/ServerDetails.module.css +118 -0
- package/components/ServerDetails.tsx +116 -0
- package/components/TelemetryDashboard.module.css +177 -0
- package/components/TelemetryDashboard.tsx +154 -0
- package/components/ToolList.module.css +2 -2
- package/components/ToolTester.module.css +24 -119
- package/components/ToolTester.tsx +627 -229
- package/package.json +2 -2
- package/server.ts +24 -10
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { type ExecutionOptions, type ExecutionHooks, type ExecutionResult } from 'mcpgraph';
|
|
3
|
+
import { getApi } from '@/lib/mcpGraphApi';
|
|
4
|
+
import { sendExecutionEvent, closeExecutionStream } from '@/lib/executionStreamServer';
|
|
5
|
+
import { registerController, unregisterController, getController } from '@/lib/executionController';
|
|
3
6
|
|
|
4
|
-
let apiInstance: McpGraphApi | null = null;
|
|
5
|
-
|
|
6
|
-
function getApi(): McpGraphApi {
|
|
7
|
-
const configPath = process.env.MCPGRAPH_CONFIG_PATH;
|
|
8
|
-
if (!configPath) {
|
|
9
|
-
throw new Error('MCPGRAPH_CONFIG_PATH environment variable is not set');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (!apiInstance) {
|
|
13
|
-
apiInstance = new McpGraphApi(configPath);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return apiInstance;
|
|
17
|
-
}
|
|
18
7
|
|
|
19
8
|
export async function GET(
|
|
20
9
|
request: Request,
|
|
@@ -45,16 +34,278 @@ export async function POST(
|
|
|
45
34
|
request: Request,
|
|
46
35
|
{ params }: { params: { toolName: string } }
|
|
47
36
|
) {
|
|
37
|
+
let executionId: string | undefined;
|
|
48
38
|
try {
|
|
49
39
|
const api = getApi();
|
|
50
40
|
const body = await request.json();
|
|
51
41
|
const args = body.args || {};
|
|
42
|
+
executionId = body.executionId as string | undefined;
|
|
43
|
+
const executionOptions = body.options as ExecutionOptions | undefined;
|
|
44
|
+
|
|
45
|
+
// Log breakpoints received
|
|
46
|
+
const breakpointsReceived = executionOptions?.breakpoints || [];
|
|
47
|
+
console.log(`[API] Received breakpoints: ${breakpointsReceived.length > 0 ? breakpointsReceived.join(', ') : 'none'}`);
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
// If executionId is provided, set up hooks to stream events via SSE
|
|
50
|
+
// Store breakpoints for use in hooks (controller may not be available yet)
|
|
51
|
+
const breakpointsList = executionOptions?.breakpoints || [];
|
|
52
|
+
let hooks: ExecutionHooks | undefined;
|
|
53
|
+
if (executionId) {
|
|
54
|
+
const execId = executionId; // Capture in const for closure
|
|
55
|
+
console.log(`[API] Setting up hooks for executionId: ${execId}, breakpoints: ${breakpointsList.join(', ')}`);
|
|
56
|
+
hooks = {
|
|
57
|
+
onNodeStart: async (executionIndex, nodeId, node, context) => {
|
|
58
|
+
console.log(`[API] onNodeStart hook called for node: ${nodeId}, executionIndex: ${executionIndex}`);
|
|
59
|
+
|
|
60
|
+
// Check if this node should have a breakpoint
|
|
61
|
+
const controller = getController(execId);
|
|
62
|
+
if (controller && breakpointsList.includes(nodeId)) {
|
|
63
|
+
const controllerBreakpoints = controller.getBreakpoints();
|
|
64
|
+
const state = controller.getState();
|
|
65
|
+
console.log(`[API] WARNING: onNodeStart called for node ${nodeId} which has a breakpoint!`);
|
|
66
|
+
console.log(`[API] Controller breakpoints: ${controllerBreakpoints.length > 0 ? controllerBreakpoints.join(', ') : 'none'}`);
|
|
67
|
+
console.log(`[API] Controller status: ${state.status}, currentNodeId: ${state.currentNodeId}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sendExecutionEvent(execId, 'nodeStart', {
|
|
71
|
+
nodeId,
|
|
72
|
+
nodeType: node.type,
|
|
73
|
+
executionIndex,
|
|
74
|
+
context, // Send context so client can determine input
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Note: mcpGraph should check breakpoints internally before executing nodes
|
|
79
|
+
// If we reach here, the node is starting. The controller's breakpoint checking
|
|
80
|
+
// should have paused execution before this hook is called.
|
|
81
|
+
|
|
82
|
+
return true; // Continue execution
|
|
83
|
+
},
|
|
84
|
+
onNodeComplete: async (executionIndex, nodeId, node, input, output, duration) => {
|
|
85
|
+
sendExecutionEvent(execId, 'nodeComplete', {
|
|
86
|
+
nodeId,
|
|
87
|
+
nodeType: node.type,
|
|
88
|
+
executionIndex,
|
|
89
|
+
input,
|
|
90
|
+
output,
|
|
91
|
+
duration,
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
onNodeError: async (executionIndex, nodeId, node, error, context) => {
|
|
96
|
+
sendExecutionEvent(execId, 'nodeError', {
|
|
97
|
+
nodeId,
|
|
98
|
+
nodeType: node.type,
|
|
99
|
+
executionIndex,
|
|
100
|
+
input: context, // Include context as input (mcpGraph 0.1.19+ provides actual context)
|
|
101
|
+
error: {
|
|
102
|
+
message: error.message,
|
|
103
|
+
stack: error.stack,
|
|
104
|
+
},
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
onPause: async (executionIndex, nodeId, context) => {
|
|
109
|
+
console.log(`[API] onPause hook called for node: ${nodeId}, executionIndex: ${executionIndex}`);
|
|
110
|
+
|
|
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';
|
|
116
|
+
|
|
117
|
+
sendExecutionEvent(execId, 'pause', {
|
|
118
|
+
nodeId,
|
|
119
|
+
nodeType,
|
|
120
|
+
executionIndex,
|
|
121
|
+
context, // Include context so client can show input for pending node
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
// Send stateUpdate to ensure UI status is updated
|
|
125
|
+
sendExecutionEvent(execId, 'stateUpdate', {
|
|
126
|
+
status: 'paused',
|
|
127
|
+
currentNodeId: nodeId,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
onResume: async () => {
|
|
132
|
+
sendExecutionEvent(execId, 'resume', {
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Merge with provided hooks if any
|
|
140
|
+
const finalOptions: ExecutionOptions = {
|
|
141
|
+
...executionOptions,
|
|
142
|
+
hooks: executionOptions?.hooks
|
|
143
|
+
? {
|
|
144
|
+
...hooks,
|
|
145
|
+
...executionOptions.hooks,
|
|
146
|
+
// Merge hook functions - call both
|
|
147
|
+
onNodeStart: async (executionIndex, nodeId, node, context) => {
|
|
148
|
+
const hook1 = hooks?.onNodeStart;
|
|
149
|
+
const hook2 = executionOptions.hooks?.onNodeStart;
|
|
150
|
+
const result1 = hook1 ? await hook1(executionIndex, nodeId, node, context) : true;
|
|
151
|
+
const result2 = hook2 ? await hook2(executionIndex, nodeId, node, context) : true;
|
|
152
|
+
return result1 && result2;
|
|
153
|
+
},
|
|
154
|
+
onNodeComplete: async (executionIndex, nodeId, node, input, output, duration) => {
|
|
155
|
+
await hooks?.onNodeComplete?.(executionIndex, nodeId, node, input, output, duration);
|
|
156
|
+
await executionOptions.hooks?.onNodeComplete?.(executionIndex, nodeId, node, input, output, duration);
|
|
157
|
+
},
|
|
158
|
+
onNodeError: async (executionIndex, nodeId, node, error, context) => {
|
|
159
|
+
await hooks?.onNodeError?.(executionIndex, nodeId, node, error, context);
|
|
160
|
+
await executionOptions.hooks?.onNodeError?.(executionIndex, nodeId, node, error, context);
|
|
161
|
+
},
|
|
162
|
+
onPause: async (executionIndex, nodeId, context) => {
|
|
163
|
+
await hooks?.onPause?.(executionIndex, nodeId, context);
|
|
164
|
+
await executionOptions.hooks?.onPause?.(executionIndex, nodeId, context);
|
|
165
|
+
},
|
|
166
|
+
onResume: async () => {
|
|
167
|
+
await hooks?.onResume?.();
|
|
168
|
+
await executionOptions.hooks?.onResume?.();
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
: hooks,
|
|
172
|
+
breakpoints: executionOptions?.breakpoints,
|
|
173
|
+
enableTelemetry: executionOptions?.enableTelemetry ?? true, // Enable by default for UX
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
console.log(`[API] Executing tool ${params.toolName} with executionId: ${executionId || 'none'}`);
|
|
177
|
+
const finalBreakpoints = finalOptions.breakpoints || [];
|
|
178
|
+
console.log(`[API] Final options breakpoints: ${finalBreakpoints.length > 0 ? finalBreakpoints.join(', ') : 'none'}`);
|
|
179
|
+
console.log(`[API] Final options object:`, JSON.stringify({
|
|
180
|
+
breakpoints: finalBreakpoints,
|
|
181
|
+
enableTelemetry: finalOptions.enableTelemetry,
|
|
182
|
+
hasHooks: !!finalOptions.hooks
|
|
183
|
+
}, null, 2));
|
|
54
184
|
|
|
55
|
-
|
|
185
|
+
// Start execution and get controller directly (mcpGraph 0.1.11+ returns both)
|
|
186
|
+
const { promise: executionPromise, controller } = api.executeTool(params.toolName, args, finalOptions);
|
|
187
|
+
|
|
188
|
+
console.log(`[API] executeTool returned controller: ${controller ? 'present' : 'null'}, executionId: ${executionId || 'none'}`);
|
|
189
|
+
|
|
190
|
+
// Register controller immediately if we have executionId and controller
|
|
191
|
+
if (executionId && controller) {
|
|
192
|
+
const execId = executionId; // Capture in const for closure
|
|
193
|
+
registerController(execId, controller);
|
|
194
|
+
console.log(`[API] Registered controller for executionId: ${execId}`);
|
|
195
|
+
|
|
196
|
+
// Log breakpoints on controller
|
|
197
|
+
const controllerBreakpoints = controller.getBreakpoints();
|
|
198
|
+
console.log(`[API] Controller breakpoints: ${controllerBreakpoints.length > 0 ? controllerBreakpoints.join(', ') : 'none'}`);
|
|
199
|
+
} else {
|
|
200
|
+
if (!executionId) {
|
|
201
|
+
console.log(`[API] WARNING: No executionId provided, controller not registered`);
|
|
202
|
+
}
|
|
203
|
+
if (!controller) {
|
|
204
|
+
console.log(`[API] WARNING: Controller is null, cannot register. Hooks: ${!!finalOptions.hooks}, Breakpoints: ${finalBreakpoints.length > 0 ? finalBreakpoints.join(', ') : 'none'}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = await executionPromise;
|
|
209
|
+
console.log(`[API] Tool execution completed, result:`, result.result ? 'present' : 'missing');
|
|
210
|
+
|
|
211
|
+
// Send completion event and close stream
|
|
212
|
+
if (executionId) {
|
|
213
|
+
const execId = executionId; // Capture in const for closure
|
|
214
|
+
console.log(`[API] Sending executionComplete event for executionId: ${execId}`);
|
|
215
|
+
|
|
216
|
+
// Fetch input contexts for all execution records BEFORE unregistering controller
|
|
217
|
+
// Use the controller's context directly since api.getContextForExecution() requires active controller
|
|
218
|
+
const controller = getController(execId);
|
|
219
|
+
console.log(`[API] Fetching input contexts for ${result.executionHistory?.length || 0} records`);
|
|
220
|
+
const executionHistoryWithInput = (result.executionHistory || []).map((record) => {
|
|
221
|
+
console.log(`[API] Processing record: nodeId=${record.nodeId}, executionIndex=${record.executionIndex}`);
|
|
222
|
+
try {
|
|
223
|
+
let context: Record<string, unknown> | null = null;
|
|
224
|
+
if (controller) {
|
|
225
|
+
const state = controller.getState();
|
|
226
|
+
context = state.context.getContextForExecution(record.executionIndex);
|
|
227
|
+
} else {
|
|
228
|
+
console.warn(`[API] Controller not found for ${execId}, trying API method`);
|
|
229
|
+
context = api.getContextForExecution(record.executionIndex);
|
|
230
|
+
}
|
|
231
|
+
console.log(`[API] Got context for ${record.nodeId}:`, context ? 'present' : 'null', context);
|
|
232
|
+
return {
|
|
233
|
+
...record,
|
|
234
|
+
input: context || undefined,
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`[API] Error getting context for executionIndex ${record.executionIndex}:`, error);
|
|
238
|
+
return record;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
console.log(`[API] Final execution history with input:`, JSON.stringify(executionHistoryWithInput, null, 2));
|
|
242
|
+
|
|
243
|
+
sendExecutionEvent(execId, 'executionComplete', {
|
|
244
|
+
result: result.result,
|
|
245
|
+
executionHistory: executionHistoryWithInput,
|
|
246
|
+
telemetry: result.telemetry
|
|
247
|
+
? {
|
|
248
|
+
...result.telemetry,
|
|
249
|
+
nodeDurations: Object.fromEntries(result.telemetry.nodeDurations),
|
|
250
|
+
nodeCounts: Object.fromEntries(result.telemetry.nodeCounts),
|
|
251
|
+
}
|
|
252
|
+
: undefined,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
// Close stream immediately - enqueue is synchronous, event is already sent
|
|
256
|
+
console.log(`[API] Closing stream for executionId: ${execId}`);
|
|
257
|
+
closeExecutionStream(execId);
|
|
258
|
+
unregisterController(execId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Serialize telemetry Maps for JSON response
|
|
262
|
+
const responseResult = {
|
|
263
|
+
...result,
|
|
264
|
+
telemetry: result.telemetry
|
|
265
|
+
? {
|
|
266
|
+
...result.telemetry,
|
|
267
|
+
nodeDurations: Object.fromEntries(result.telemetry.nodeDurations),
|
|
268
|
+
nodeCounts: Object.fromEntries(result.telemetry.nodeCounts),
|
|
269
|
+
}
|
|
270
|
+
: undefined,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return NextResponse.json({ result: responseResult });
|
|
56
274
|
} catch (error) {
|
|
57
275
|
console.error('Error executing tool:', error);
|
|
276
|
+
|
|
277
|
+
// Check if execution was stopped (not a real error)
|
|
278
|
+
const isStopped = error instanceof Error && error.message === 'Execution was stopped';
|
|
279
|
+
|
|
280
|
+
if (executionId) {
|
|
281
|
+
const execId = executionId; // Capture in const for closure
|
|
282
|
+
|
|
283
|
+
if (isStopped) {
|
|
284
|
+
// Execution was stopped by user - send stopped event, not error
|
|
285
|
+
sendExecutionEvent(execId, 'executionStopped', {
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
// Real error occurred
|
|
290
|
+
sendExecutionEvent(execId, 'executionError', {
|
|
291
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Close stream immediately - enqueue is synchronous, event is already sent
|
|
297
|
+
closeExecutionStream(execId);
|
|
298
|
+
unregisterController(execId);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// If stopped, return success (stopping is intentional)
|
|
302
|
+
if (isStopped) {
|
|
303
|
+
return NextResponse.json({
|
|
304
|
+
result: null,
|
|
305
|
+
stopped: true
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
58
309
|
return NextResponse.json(
|
|
59
310
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
60
311
|
{ status: 500 }
|
package/app/api/tools/route.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { getApi } from '@/lib/mcpGraphApi';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function getApi(): McpGraphApi {
|
|
7
|
-
const configPath = process.env.MCPGRAPH_CONFIG_PATH;
|
|
8
|
-
if (!configPath) {
|
|
9
|
-
throw new Error('MCPGRAPH_CONFIG_PATH environment variable is not set');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (!apiInstance) {
|
|
13
|
-
apiInstance = new McpGraphApi(configPath);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return apiInstance;
|
|
17
|
-
}
|
|
4
|
+
// Force dynamic rendering - this route requires runtime config
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
18
6
|
|
|
19
7
|
export async function GET() {
|
|
20
8
|
try {
|
package/app/page.module.css
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
width: 300px;
|
|
34
34
|
background-color: #fff;
|
|
35
35
|
border-right: 1px solid #e0e0e0;
|
|
36
|
-
|
|
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
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
flex-direction: column;
|
|
48
|
+
.testerSection {
|
|
49
|
+
background-color: #fff;
|
|
50
|
+
padding: 0.75rem 1.5rem;
|
|
50
51
|
border-bottom: 1px solid #e0e0e0;
|
|
51
|
-
|
|
52
|
+
flex-shrink: 0;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
max-height: 400px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
min-width: 0;
|
|
68
106
|
}
|
|
69
107
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
color: #
|
|
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 {
|
|
@@ -19,9 +20,12 @@ export default function Home() {
|
|
|
19
20
|
const [graphData, setGraphData] = useState<{ nodes: any[]; edges: any[] } | 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
|
|
28
|
+
// Load tools and graph data (graph includes config)
|
|
25
29
|
Promise.all([
|
|
26
30
|
fetch('/api/tools').then(res => res.json()),
|
|
27
31
|
fetch('/api/graph').then(res => res.json()),
|
|
@@ -37,6 +41,9 @@ export default function Home() {
|
|
|
37
41
|
}
|
|
38
42
|
setTools(toolsRes.tools);
|
|
39
43
|
setGraphData({ nodes: graphRes.nodes, edges: graphRes.edges });
|
|
44
|
+
if (graphRes.config) {
|
|
45
|
+
setServerDetails(graphRes.config);
|
|
46
|
+
}
|
|
40
47
|
if (toolsRes.tools.length > 0) {
|
|
41
48
|
setSelectedTool(toolsRes.tools[0].name);
|
|
42
49
|
}
|
|
@@ -77,31 +84,47 @@ export default function Home() {
|
|
|
77
84
|
|
|
78
85
|
<div className={styles.main}>
|
|
79
86
|
<div className={styles.sidebar}>
|
|
87
|
+
{serverDetails && <ServerConfig config={serverDetails} />}
|
|
80
88
|
<ToolList
|
|
81
89
|
tools={tools}
|
|
82
90
|
selectedTool={selectedTool}
|
|
83
91
|
onSelectTool={setSelectedTool}
|
|
84
92
|
/>
|
|
93
|
+
{serverDetails && <McpServers config={serverDetails} />}
|
|
85
94
|
</div>
|
|
86
95
|
|
|
87
96
|
<div className={styles.content}>
|
|
88
|
-
|
|
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
|
-
|
|
97
|
+
{/* Top section: Input form */}
|
|
99
98
|
{selectedTool && (
|
|
100
99
|
<div className={styles.testerSection}>
|
|
101
100
|
<h2>Test Tool: {selectedTool}</h2>
|
|
102
|
-
<
|
|
101
|
+
<InputForm
|
|
102
|
+
ref={inputFormRef}
|
|
103
|
+
toolName={selectedTool}
|
|
104
|
+
onSubmit={(data, startPaused) => {
|
|
105
|
+
// Form validated and collected data - pass to ToolTester for execution
|
|
106
|
+
if (toolTesterFormSubmitRef.current) {
|
|
107
|
+
toolTesterFormSubmitRef.current(data, startPaused);
|
|
108
|
+
} else {
|
|
109
|
+
console.warn('[page.tsx] toolTesterFormSubmitRef.current is null');
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
103
113
|
</div>
|
|
104
114
|
)}
|
|
115
|
+
|
|
116
|
+
{/* Bottom section: Execution/testing area (includes debug controls, graph, and history) */}
|
|
117
|
+
{selectedTool && graphData && (
|
|
118
|
+
<ToolTester
|
|
119
|
+
toolName={selectedTool}
|
|
120
|
+
graphData={graphData}
|
|
121
|
+
inputFormRef={inputFormRef}
|
|
122
|
+
onFormSubmit={(handler) => {
|
|
123
|
+
// Store the handler that InputForm will call
|
|
124
|
+
toolTesterFormSubmitRef.current = handler;
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
105
128
|
</div>
|
|
106
129
|
</div>
|
|
107
130
|
</div>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
padding: 12px;
|
|
6
|
+
background: #f5f5f5;
|
|
7
|
+
border: none;
|
|
8
|
+
border-radius: 0;
|
|
9
|
+
margin-bottom: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.controlsRow {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
gap: 16px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.statusInfo {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 8px;
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
margin-left: auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.statusLabel {
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
color: #666;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.statusBadge {
|
|
33
|
+
padding: 4px 8px;
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
font-size: 11px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
letter-spacing: 0.5px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.statusBadge.not_started {
|
|
42
|
+
background: #e0e0e0;
|
|
43
|
+
color: #666;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.statusBadge.running {
|
|
47
|
+
background: #e3f2fd;
|
|
48
|
+
color: #1976d2;
|
|
49
|
+
animation: pulse 2s ease-in-out infinite;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.statusBadge.paused {
|
|
53
|
+
background: #fff3e0;
|
|
54
|
+
color: #f57c00;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.statusBadge.finished {
|
|
58
|
+
background: #e8f5e9;
|
|
59
|
+
color: #2e7d32;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.statusBadge.error {
|
|
63
|
+
background: #ffebee;
|
|
64
|
+
color: #c62828;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@keyframes pulse {
|
|
68
|
+
0%, 100% {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
50% {
|
|
72
|
+
opacity: 0.7;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.separator {
|
|
77
|
+
color: #999;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.currentNode {
|
|
81
|
+
color: #666;
|
|
82
|
+
font-size: 12px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.currentNode code {
|
|
86
|
+
background: #fff;
|
|
87
|
+
padding: 2px 6px;
|
|
88
|
+
border-radius: 3px;
|
|
89
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
color: #333;
|
|
92
|
+
border: 1px solid #ddd;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.controls {
|
|
96
|
+
display: flex;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.button {
|
|
101
|
+
padding: 8px 16px;
|
|
102
|
+
background: #2196f3;
|
|
103
|
+
color: white;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
font-weight: 500;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
transition: background-color 0.2s;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 6px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.button:hover:not(:disabled) {
|
|
116
|
+
background: #1976d2;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.button:disabled {
|
|
120
|
+
background: #ccc;
|
|
121
|
+
cursor: not-allowed;
|
|
122
|
+
opacity: 0.6;
|
|
123
|
+
}
|
|
124
|
+
|