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,283 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import styles from './ToolTester.module.css';
|
|
5
|
+
|
|
6
|
+
interface ToolTesterProps {
|
|
7
|
+
toolName: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ToolInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties?: Record<string, any>;
|
|
16
|
+
required?: string[];
|
|
17
|
+
};
|
|
18
|
+
outputSchema?: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties?: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function ToolTester({ toolName }: ToolTesterProps) {
|
|
25
|
+
const [toolInfo, setToolInfo] = useState<ToolInfo | null>(null);
|
|
26
|
+
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
27
|
+
const [result, setResult] = useState<any>(null);
|
|
28
|
+
const [loading, setLoading] = useState(false);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Load tool info
|
|
33
|
+
fetch(`/api/tools/${toolName}`)
|
|
34
|
+
.then(res => res.json())
|
|
35
|
+
.then(data => {
|
|
36
|
+
if (data.error) {
|
|
37
|
+
setError(data.error);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setToolInfo(data.tool);
|
|
41
|
+
// Initialize form data with default values
|
|
42
|
+
const defaults: Record<string, any> = {};
|
|
43
|
+
if (data.tool.inputSchema.properties) {
|
|
44
|
+
Object.entries(data.tool.inputSchema.properties).forEach(([key, prop]: [string, any]) => {
|
|
45
|
+
if (prop.type === 'string') {
|
|
46
|
+
defaults[key] = '';
|
|
47
|
+
} else if (prop.type === 'number') {
|
|
48
|
+
defaults[key] = 0;
|
|
49
|
+
} else if (prop.type === 'boolean') {
|
|
50
|
+
defaults[key] = false;
|
|
51
|
+
} else if (prop.type === 'array') {
|
|
52
|
+
defaults[key] = [];
|
|
53
|
+
} else if (prop.type === 'object') {
|
|
54
|
+
defaults[key] = {};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
setFormData(defaults);
|
|
59
|
+
})
|
|
60
|
+
.catch(err => {
|
|
61
|
+
setError(err.message);
|
|
62
|
+
});
|
|
63
|
+
}, [toolName]);
|
|
64
|
+
|
|
65
|
+
const handleInputChange = (key: string, value: any) => {
|
|
66
|
+
setFormData(prev => ({
|
|
67
|
+
...prev,
|
|
68
|
+
[key]: value,
|
|
69
|
+
}));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
setResult(null);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`/api/tools/${toolName}`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({ args: formData }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = await response.json();
|
|
88
|
+
|
|
89
|
+
if (data.error) {
|
|
90
|
+
setError(data.error);
|
|
91
|
+
} else {
|
|
92
|
+
setResult(data.result);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
96
|
+
} finally {
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderInputField = (key: string, prop: any) => {
|
|
102
|
+
const isRequired = toolInfo?.inputSchema.required?.includes(key);
|
|
103
|
+
const value = formData[key] ?? '';
|
|
104
|
+
|
|
105
|
+
switch (prop.type) {
|
|
106
|
+
case 'string':
|
|
107
|
+
return (
|
|
108
|
+
<div key={key} className={styles.field}>
|
|
109
|
+
<label className={styles.label}>
|
|
110
|
+
{key}
|
|
111
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
112
|
+
</label>
|
|
113
|
+
<input
|
|
114
|
+
type="text"
|
|
115
|
+
value={value}
|
|
116
|
+
onChange={e => handleInputChange(key, e.target.value)}
|
|
117
|
+
className={styles.input}
|
|
118
|
+
placeholder={prop.description || `Enter ${key}`}
|
|
119
|
+
/>
|
|
120
|
+
{prop.description && (
|
|
121
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
case 'number':
|
|
126
|
+
return (
|
|
127
|
+
<div key={key} className={styles.field}>
|
|
128
|
+
<label className={styles.label}>
|
|
129
|
+
{key}
|
|
130
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
131
|
+
</label>
|
|
132
|
+
<input
|
|
133
|
+
type="number"
|
|
134
|
+
value={value}
|
|
135
|
+
onChange={e => handleInputChange(key, parseFloat(e.target.value) || 0)}
|
|
136
|
+
className={styles.input}
|
|
137
|
+
placeholder={prop.description || `Enter ${key}`}
|
|
138
|
+
/>
|
|
139
|
+
{prop.description && (
|
|
140
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
case 'boolean':
|
|
145
|
+
return (
|
|
146
|
+
<div key={key} className={styles.field}>
|
|
147
|
+
<label className={styles.checkboxLabel}>
|
|
148
|
+
<input
|
|
149
|
+
type="checkbox"
|
|
150
|
+
checked={value}
|
|
151
|
+
onChange={e => handleInputChange(key, e.target.checked)}
|
|
152
|
+
className={styles.checkbox}
|
|
153
|
+
/>
|
|
154
|
+
{key}
|
|
155
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
156
|
+
</label>
|
|
157
|
+
{prop.description && (
|
|
158
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
case 'array':
|
|
163
|
+
return (
|
|
164
|
+
<div key={key} className={styles.field}>
|
|
165
|
+
<label className={styles.label}>
|
|
166
|
+
{key}
|
|
167
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
168
|
+
</label>
|
|
169
|
+
<textarea
|
|
170
|
+
value={Array.isArray(value) ? JSON.stringify(value, null, 2) : '[]'}
|
|
171
|
+
onChange={e => {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(e.target.value);
|
|
174
|
+
handleInputChange(key, parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
// Invalid JSON, ignore
|
|
177
|
+
}
|
|
178
|
+
}}
|
|
179
|
+
className={styles.textarea}
|
|
180
|
+
placeholder={prop.description || `Enter ${key} as JSON array`}
|
|
181
|
+
rows={3}
|
|
182
|
+
/>
|
|
183
|
+
{prop.description && (
|
|
184
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
case 'object':
|
|
189
|
+
return (
|
|
190
|
+
<div key={key} className={styles.field}>
|
|
191
|
+
<label className={styles.label}>
|
|
192
|
+
{key}
|
|
193
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
194
|
+
</label>
|
|
195
|
+
<textarea
|
|
196
|
+
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : '{}'}
|
|
197
|
+
onChange={e => {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(e.target.value);
|
|
200
|
+
handleInputChange(key, parsed);
|
|
201
|
+
} catch {
|
|
202
|
+
// Invalid JSON, ignore
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
className={styles.textarea}
|
|
206
|
+
placeholder={prop.description || `Enter ${key} as JSON object`}
|
|
207
|
+
rows={5}
|
|
208
|
+
/>
|
|
209
|
+
{prop.description && (
|
|
210
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
default:
|
|
215
|
+
return (
|
|
216
|
+
<div key={key} className={styles.field}>
|
|
217
|
+
<label className={styles.label}>
|
|
218
|
+
{key}
|
|
219
|
+
{isRequired && <span className={styles.required}>*</span>}
|
|
220
|
+
</label>
|
|
221
|
+
<textarea
|
|
222
|
+
value={typeof value === 'string' ? value : JSON.stringify(value)}
|
|
223
|
+
onChange={e => {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(e.target.value);
|
|
226
|
+
handleInputChange(key, parsed);
|
|
227
|
+
} catch {
|
|
228
|
+
handleInputChange(key, e.target.value);
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
231
|
+
className={styles.textarea}
|
|
232
|
+
placeholder={prop.description || `Enter ${key}`}
|
|
233
|
+
rows={3}
|
|
234
|
+
/>
|
|
235
|
+
{prop.description && (
|
|
236
|
+
<div className={styles.hint}>{prop.description}</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (!toolInfo) {
|
|
244
|
+
return <div className={styles.loading}>Loading tool information...</div>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className={styles.container}>
|
|
249
|
+
<form onSubmit={handleSubmit} className={styles.form}>
|
|
250
|
+
<div className={styles.inputs}>
|
|
251
|
+
{toolInfo.inputSchema.properties &&
|
|
252
|
+
Object.entries(toolInfo.inputSchema.properties).map(([key, prop]) =>
|
|
253
|
+
renderInputField(key, prop)
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<button
|
|
258
|
+
type="submit"
|
|
259
|
+
disabled={loading}
|
|
260
|
+
className={styles.submitButton}
|
|
261
|
+
>
|
|
262
|
+
{loading ? 'Testing...' : 'Test Tool'}
|
|
263
|
+
</button>
|
|
264
|
+
</form>
|
|
265
|
+
|
|
266
|
+
{error && (
|
|
267
|
+
<div className={styles.error}>
|
|
268
|
+
<strong>Error:</strong> {error}
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{result && (
|
|
273
|
+
<div className={styles.result}>
|
|
274
|
+
<h3>Result:</h3>
|
|
275
|
+
<pre className={styles.resultContent}>
|
|
276
|
+
{JSON.stringify(result, null, 2)}
|
|
277
|
+
</pre>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
package/next.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcpgraph-ux",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual interface for mcpGraph - visualize and test MCP tool execution graphs",
|
|
5
|
+
"main": "server.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcpgraph-ux": "./server.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "next dev",
|
|
11
|
+
"build": "next build",
|
|
12
|
+
"start": "next start",
|
|
13
|
+
"lint": "next lint",
|
|
14
|
+
"server": "tsx server.ts",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"app",
|
|
19
|
+
"components",
|
|
20
|
+
"server.ts",
|
|
21
|
+
"next.config.js",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"package.json"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"next": "^14.2.0",
|
|
29
|
+
"react": "^18.3.0",
|
|
30
|
+
"react-dom": "^18.3.0",
|
|
31
|
+
"reactflow": "^11.11.0",
|
|
32
|
+
"mcpgraph": "^0.1.4",
|
|
33
|
+
"zod": "^3.22.4",
|
|
34
|
+
"dagre": "^0.8.5",
|
|
35
|
+
"tsx": "^4.7.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
39
|
+
"@types/react": "^18.3.0",
|
|
40
|
+
"@types/react-dom": "^18.3.0",
|
|
41
|
+
"@types/dagre": "^0.7.52",
|
|
42
|
+
"typescript": "^5.3.3",
|
|
43
|
+
"eslint": "^8.57.0",
|
|
44
|
+
"eslint-config-next": "^14.2.0"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"mcp",
|
|
48
|
+
"model-context-protocol",
|
|
49
|
+
"graph",
|
|
50
|
+
"workflow",
|
|
51
|
+
"visualization",
|
|
52
|
+
"react-flow",
|
|
53
|
+
"dagre",
|
|
54
|
+
"nextjs"
|
|
55
|
+
],
|
|
56
|
+
"author": "TeamSpark, LLC <support@teamspark.ai>",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/TeamSparkAI/mcpGraph-ux.git"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
package/server.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server entry point for mcpGraph UX
|
|
5
|
+
*
|
|
6
|
+
* Usage: npm run server <port> <config-path>
|
|
7
|
+
* Example: npm run server 3000 ../mcpGraph/examples/count_files.yaml
|
|
8
|
+
*
|
|
9
|
+
* Or with tsx directly:
|
|
10
|
+
* tsx server.ts 3000 ../mcpGraph/examples/count_files.yaml
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createServer } from 'http';
|
|
14
|
+
import { parse } from 'url';
|
|
15
|
+
import next from 'next';
|
|
16
|
+
import { resolve } from 'path';
|
|
17
|
+
|
|
18
|
+
const port = parseInt(process.argv[2] || '3000', 10);
|
|
19
|
+
const configPathArg = process.argv[3];
|
|
20
|
+
|
|
21
|
+
if (!configPathArg) {
|
|
22
|
+
console.error('Usage: npm run server <port> <config-path>');
|
|
23
|
+
console.error('Example: npm run server 3000 ../mcpGraph/examples/count_files.yaml');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Resolve config path to absolute path
|
|
28
|
+
const configPath = resolve(process.cwd(), configPathArg);
|
|
29
|
+
|
|
30
|
+
// Store config path in environment variable for API routes
|
|
31
|
+
process.env.MCPGRAPH_CONFIG_PATH = configPath;
|
|
32
|
+
|
|
33
|
+
const dev = process.env.NODE_ENV !== 'production';
|
|
34
|
+
const app = next({ dev });
|
|
35
|
+
const handle = app.getRequestHandler();
|
|
36
|
+
|
|
37
|
+
app.prepare().then(() => {
|
|
38
|
+
createServer((req, res) => {
|
|
39
|
+
const parsedUrl = parse(req.url!, true);
|
|
40
|
+
handle(req, res, parsedUrl);
|
|
41
|
+
}).listen(port, (err?: Error) => {
|
|
42
|
+
if (err) throw err;
|
|
43
|
+
console.log(`> Ready on http://localhost:${port}`);
|
|
44
|
+
console.log(`> Config path: ${configPath}`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
28
|
+
|