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.
@@ -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
+