mcpgraph-ux 0.1.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/app/api/graph/route.ts +114 -0
- package/app/api/tools/[toolName]/route.ts +64 -0
- package/app/api/tools/route.ts +32 -0
- package/app/globals.css +26 -0
- package/app/layout.tsx +20 -0
- package/app/page.module.css +94 -0
- package/app/page.tsx +110 -0
- package/components/GraphVisualization.module.css +6 -0
- package/components/GraphVisualization.tsx +276 -0
- package/components/ToolList.module.css +49 -0
- package/components/ToolList.tsx +43 -0
- package/components/ToolTester.module.css +142 -0
- package/components/ToolTester.tsx +283 -0
- package/next.config.js +7 -0
- package/package.json +66 -0
- package/server.ts +47 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
|
+
import ReactFlow, {
|
|
5
|
+
Background,
|
|
6
|
+
Controls,
|
|
7
|
+
MiniMap,
|
|
8
|
+
Node,
|
|
9
|
+
Edge,
|
|
10
|
+
useNodesState,
|
|
11
|
+
useEdgesState,
|
|
12
|
+
ConnectionMode,
|
|
13
|
+
MarkerType,
|
|
14
|
+
Handle,
|
|
15
|
+
Position,
|
|
16
|
+
} from 'reactflow';
|
|
17
|
+
import dagre from 'dagre';
|
|
18
|
+
import 'reactflow/dist/style.css';
|
|
19
|
+
import styles from './GraphVisualization.module.css';
|
|
20
|
+
|
|
21
|
+
interface GraphVisualizationProps {
|
|
22
|
+
nodes: Node[];
|
|
23
|
+
edges: Edge[];
|
|
24
|
+
selectedTool?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Custom node styles based on node type
|
|
28
|
+
const getNodeStyle = (nodeType: string) => {
|
|
29
|
+
const baseStyle = {
|
|
30
|
+
padding: '10px',
|
|
31
|
+
borderRadius: '8px',
|
|
32
|
+
border: '2px solid',
|
|
33
|
+
fontSize: '12px',
|
|
34
|
+
fontWeight: 500,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
switch (nodeType) {
|
|
38
|
+
case 'entry':
|
|
39
|
+
return {
|
|
40
|
+
...baseStyle,
|
|
41
|
+
backgroundColor: '#e8f5e9',
|
|
42
|
+
borderColor: '#4caf50',
|
|
43
|
+
color: '#2e7d32',
|
|
44
|
+
};
|
|
45
|
+
case 'exit':
|
|
46
|
+
return {
|
|
47
|
+
...baseStyle,
|
|
48
|
+
backgroundColor: '#fff3e0',
|
|
49
|
+
borderColor: '#ff9800',
|
|
50
|
+
color: '#e65100',
|
|
51
|
+
};
|
|
52
|
+
case 'mcp':
|
|
53
|
+
return {
|
|
54
|
+
...baseStyle,
|
|
55
|
+
backgroundColor: '#e3f2fd',
|
|
56
|
+
borderColor: '#2196f3',
|
|
57
|
+
color: '#1565c0',
|
|
58
|
+
};
|
|
59
|
+
case 'transform':
|
|
60
|
+
return {
|
|
61
|
+
...baseStyle,
|
|
62
|
+
backgroundColor: '#f3e5f5',
|
|
63
|
+
borderColor: '#9c27b0',
|
|
64
|
+
color: '#6a1b9a',
|
|
65
|
+
};
|
|
66
|
+
case 'switch':
|
|
67
|
+
return {
|
|
68
|
+
...baseStyle,
|
|
69
|
+
backgroundColor: '#fce4ec',
|
|
70
|
+
borderColor: '#e91e63',
|
|
71
|
+
color: '#c2185b',
|
|
72
|
+
};
|
|
73
|
+
default:
|
|
74
|
+
return {
|
|
75
|
+
...baseStyle,
|
|
76
|
+
backgroundColor: '#f5f5f5',
|
|
77
|
+
borderColor: '#9e9e9e',
|
|
78
|
+
color: '#616161',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Custom node component with left/right handles for horizontal flow
|
|
84
|
+
function CustomNode({ data }: { data: any }) {
|
|
85
|
+
const nodeType = data.nodeType || 'unknown';
|
|
86
|
+
const style = getNodeStyle(nodeType);
|
|
87
|
+
const isEntry = nodeType === 'entry';
|
|
88
|
+
const isExit = nodeType === 'exit';
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div style={style}>
|
|
92
|
+
{!isEntry && (
|
|
93
|
+
<Handle
|
|
94
|
+
type="target"
|
|
95
|
+
position={Position.Left}
|
|
96
|
+
style={{ background: '#555' }}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
<div>{data.label}</div>
|
|
100
|
+
{!isExit && (
|
|
101
|
+
<Handle
|
|
102
|
+
type="source"
|
|
103
|
+
position={Position.Right}
|
|
104
|
+
style={{ background: '#555' }}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nodeTypes = {
|
|
112
|
+
custom: CustomNode,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Dagre layout algorithm for automatic graph positioning
|
|
116
|
+
const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
117
|
+
// Create a new dagre graph
|
|
118
|
+
const g = new dagre.graphlib.Graph();
|
|
119
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
120
|
+
g.setGraph({
|
|
121
|
+
rankdir: 'LR', // Left to right layout
|
|
122
|
+
nodesep: 100, // Horizontal spacing between nodes
|
|
123
|
+
ranksep: 150, // Vertical spacing between ranks
|
|
124
|
+
marginx: 50,
|
|
125
|
+
marginy: 50,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Add nodes to dagre graph
|
|
129
|
+
nodes.forEach(node => {
|
|
130
|
+
// Estimate node dimensions (dagre needs width/height)
|
|
131
|
+
const nodeType = (node.data as any)?.nodeType || 'unknown';
|
|
132
|
+
const label = node.id;
|
|
133
|
+
// Rough estimate: ~10px per character + padding
|
|
134
|
+
const width = Math.max(150, label.length * 8 + 40);
|
|
135
|
+
const height = 80;
|
|
136
|
+
|
|
137
|
+
g.setNode(node.id, { width, height });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Add edges to dagre graph
|
|
141
|
+
edges.forEach(edge => {
|
|
142
|
+
g.setEdge(edge.source, edge.target);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Run dagre layout
|
|
146
|
+
dagre.layout(g);
|
|
147
|
+
|
|
148
|
+
// Map dagre positions back to React Flow nodes
|
|
149
|
+
const positionedNodes = nodes.map(node => {
|
|
150
|
+
const nodeType = (node.data as any)?.nodeType || 'unknown';
|
|
151
|
+
const dagreNode = g.node(node.id);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
...node,
|
|
155
|
+
type: 'custom', // Use custom node type with left/right handles
|
|
156
|
+
position: {
|
|
157
|
+
x: dagreNode.x - (dagreNode.width / 2), // Center the node
|
|
158
|
+
y: dagreNode.y - (dagreNode.height / 2),
|
|
159
|
+
},
|
|
160
|
+
data: {
|
|
161
|
+
...node.data,
|
|
162
|
+
label: node.id,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return positionedNodes;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default function GraphVisualization({
|
|
171
|
+
nodes,
|
|
172
|
+
edges,
|
|
173
|
+
selectedTool,
|
|
174
|
+
}: GraphVisualizationProps) {
|
|
175
|
+
const layoutedNodes = useMemo(() => layoutNodes(nodes, edges), [nodes, edges]);
|
|
176
|
+
|
|
177
|
+
const [flowNodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
|
|
178
|
+
const [flowEdges, setEdges, onEdgesChange] = useEdgesState(
|
|
179
|
+
edges.map(edge => ({
|
|
180
|
+
...edge,
|
|
181
|
+
sourceHandle: null, // Use default right handle
|
|
182
|
+
targetHandle: null, // Use default left handle
|
|
183
|
+
markerEnd: {
|
|
184
|
+
type: MarkerType.ArrowClosed,
|
|
185
|
+
},
|
|
186
|
+
style: { strokeWidth: 2 },
|
|
187
|
+
}))
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const layouted = layoutNodes(nodes, edges);
|
|
192
|
+
setNodes(layouted);
|
|
193
|
+
setEdges(
|
|
194
|
+
edges.map(edge => ({
|
|
195
|
+
...edge,
|
|
196
|
+
sourceHandle: null, // Use default right handle
|
|
197
|
+
targetHandle: null, // Use default left handle
|
|
198
|
+
markerEnd: {
|
|
199
|
+
type: MarkerType.ArrowClosed,
|
|
200
|
+
},
|
|
201
|
+
style: { strokeWidth: 2 },
|
|
202
|
+
}))
|
|
203
|
+
);
|
|
204
|
+
}, [nodes, edges, setNodes, setEdges]);
|
|
205
|
+
|
|
206
|
+
// Filter nodes/edges for selected tool if provided
|
|
207
|
+
const filteredNodes = useMemo(() => {
|
|
208
|
+
if (!selectedTool) return flowNodes;
|
|
209
|
+
return flowNodes.filter(node => {
|
|
210
|
+
const data = node.data as any;
|
|
211
|
+
return (
|
|
212
|
+
(data.nodeType === 'entry' && data.tool === selectedTool) ||
|
|
213
|
+
(data.nodeType === 'exit' && data.tool === selectedTool) ||
|
|
214
|
+
flowEdges.some(edge => {
|
|
215
|
+
// Include nodes that are reachable from entry or lead to exit
|
|
216
|
+
const entryNode = flowNodes.find(
|
|
217
|
+
n => (n.data as any)?.nodeType === 'entry' && (n.data as any)?.tool === selectedTool
|
|
218
|
+
);
|
|
219
|
+
const exitNode = flowNodes.find(
|
|
220
|
+
n => (n.data as any)?.nodeType === 'exit' && (n.data as any)?.tool === selectedTool
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!entryNode || !exitNode) return false;
|
|
224
|
+
|
|
225
|
+
// Simple reachability check
|
|
226
|
+
const visited = new Set<string>();
|
|
227
|
+
const queue = [entryNode.id];
|
|
228
|
+
visited.add(entryNode.id);
|
|
229
|
+
|
|
230
|
+
while (queue.length > 0) {
|
|
231
|
+
const current = queue.shift()!;
|
|
232
|
+
if (current === node.id) return true;
|
|
233
|
+
|
|
234
|
+
flowEdges
|
|
235
|
+
.filter(e => e.source === current)
|
|
236
|
+
.forEach(e => {
|
|
237
|
+
if (!visited.has(e.target)) {
|
|
238
|
+
visited.add(e.target);
|
|
239
|
+
queue.push(e.target);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
}, [flowNodes, flowEdges, selectedTool]);
|
|
249
|
+
|
|
250
|
+
const filteredEdges = useMemo(() => {
|
|
251
|
+
if (!selectedTool) return flowEdges;
|
|
252
|
+
const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
|
|
253
|
+
return flowEdges.filter(
|
|
254
|
+
edge => filteredNodeIds.has(edge.source) && filteredNodeIds.has(edge.target)
|
|
255
|
+
);
|
|
256
|
+
}, [flowEdges, filteredNodes, selectedTool]);
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div className={styles.container}>
|
|
260
|
+
<ReactFlow
|
|
261
|
+
nodes={filteredNodes}
|
|
262
|
+
edges={filteredEdges}
|
|
263
|
+
onNodesChange={onNodesChange}
|
|
264
|
+
onEdgesChange={onEdgesChange}
|
|
265
|
+
connectionMode={ConnectionMode.Loose}
|
|
266
|
+
nodeTypes={nodeTypes}
|
|
267
|
+
fitView
|
|
268
|
+
>
|
|
269
|
+
<Background />
|
|
270
|
+
<Controls />
|
|
271
|
+
<MiniMap />
|
|
272
|
+
</ReactFlow>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.title {
|
|
8
|
+
padding: 1rem 1.5rem;
|
|
9
|
+
font-size: 1.1rem;
|
|
10
|
+
font-weight: 600;
|
|
11
|
+
color: #333;
|
|
12
|
+
border-bottom: 1px solid #e0e0e0;
|
|
13
|
+
background-color: #fafafa;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.toolList {
|
|
17
|
+
flex: 1;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.toolItem {
|
|
22
|
+
padding: 1rem 1.5rem;
|
|
23
|
+
border-bottom: 1px solid #f0f0f0;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
transition: background-color 0.2s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.toolItem:hover {
|
|
29
|
+
background-color: #f5f5f5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.toolItem.selected {
|
|
33
|
+
background-color: #e3f2fd;
|
|
34
|
+
border-left: 3px solid #2196f3;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.toolName {
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
color: #333;
|
|
40
|
+
margin-bottom: 0.25rem;
|
|
41
|
+
font-size: 0.95rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toolDescription {
|
|
45
|
+
color: #666;
|
|
46
|
+
font-size: 0.85rem;
|
|
47
|
+
line-height: 1.4;
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import styles from './ToolList.module.css';
|
|
4
|
+
|
|
5
|
+
interface Tool {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
inputSchema: Record<string, unknown>;
|
|
9
|
+
outputSchema?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ToolListProps {
|
|
13
|
+
tools: Tool[];
|
|
14
|
+
selectedTool: string | null;
|
|
15
|
+
onSelectTool: (toolName: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ToolList({
|
|
19
|
+
tools,
|
|
20
|
+
selectedTool,
|
|
21
|
+
onSelectTool,
|
|
22
|
+
}: ToolListProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.container}>
|
|
25
|
+
<h2 className={styles.title}>Tools</h2>
|
|
26
|
+
<div className={styles.toolList}>
|
|
27
|
+
{tools.map(tool => (
|
|
28
|
+
<div
|
|
29
|
+
key={tool.name}
|
|
30
|
+
className={`${styles.toolItem} ${
|
|
31
|
+
selectedTool === tool.name ? styles.selected : ''
|
|
32
|
+
}`}
|
|
33
|
+
onClick={() => onSelectTool(tool.name)}
|
|
34
|
+
>
|
|
35
|
+
<div className={styles.toolName}>{tool.name}</div>
|
|
36
|
+
<div className={styles.toolDescription}>{tool.description}</div>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1.5rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.form {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.inputs {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 1rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.field {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 0.5rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.label {
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
color: #333;
|
|
28
|
+
font-size: 0.9rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.required {
|
|
32
|
+
color: #d32f2f;
|
|
33
|
+
margin-left: 0.25rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.input,
|
|
37
|
+
.textarea {
|
|
38
|
+
padding: 0.75rem;
|
|
39
|
+
border: 1px solid #ddd;
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
font-size: 0.9rem;
|
|
42
|
+
font-family: inherit;
|
|
43
|
+
transition: border-color 0.2s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.input:focus,
|
|
47
|
+
.textarea:focus {
|
|
48
|
+
outline: none;
|
|
49
|
+
border-color: #2196f3;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.textarea {
|
|
53
|
+
resize: vertical;
|
|
54
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
55
|
+
font-size: 0.85rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.hint {
|
|
59
|
+
font-size: 0.8rem;
|
|
60
|
+
color: #666;
|
|
61
|
+
font-style: italic;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.checkboxLabel {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.5rem;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
color: #333;
|
|
70
|
+
font-size: 0.9rem;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.checkbox {
|
|
75
|
+
width: 18px;
|
|
76
|
+
height: 18px;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.submitButton {
|
|
81
|
+
padding: 0.75rem 1.5rem;
|
|
82
|
+
background-color: #2196f3;
|
|
83
|
+
color: white;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
font-size: 1rem;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: background-color 0.2s;
|
|
90
|
+
align-self: flex-start;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.submitButton:hover:not(:disabled) {
|
|
94
|
+
background-color: #1976d2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.submitButton:disabled {
|
|
98
|
+
background-color: #ccc;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.error {
|
|
103
|
+
padding: 1rem;
|
|
104
|
+
background-color: #ffebee;
|
|
105
|
+
border: 1px solid #ef5350;
|
|
106
|
+
border-radius: 4px;
|
|
107
|
+
color: #c62828;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.result {
|
|
111
|
+
padding: 1rem;
|
|
112
|
+
background-color: #f5f5f5;
|
|
113
|
+
border: 1px solid #ddd;
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.result h3 {
|
|
118
|
+
margin-bottom: 0.75rem;
|
|
119
|
+
font-size: 1rem;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: #333;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.resultContent {
|
|
125
|
+
background-color: #fff;
|
|
126
|
+
padding: 1rem;
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
overflow-x: auto;
|
|
129
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
130
|
+
font-size: 0.85rem;
|
|
131
|
+
line-height: 1.5;
|
|
132
|
+
color: #333;
|
|
133
|
+
max-height: 400px;
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.loading {
|
|
138
|
+
padding: 2rem;
|
|
139
|
+
text-align: center;
|
|
140
|
+
color: #666;
|
|
141
|
+
}
|
|
142
|
+
|