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,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
@@ -0,0 +1,7 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ }
5
+
6
+ module.exports = nextConfig
7
+
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
+