mcpgraph-ux 0.1.2 → 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.
@@ -0,0 +1,271 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
4
+ import styles from './InputForm.module.css';
5
+
6
+ interface ToolInfo {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: {
10
+ type: string;
11
+ properties?: Record<string, any>;
12
+ required?: string[];
13
+ };
14
+ outputSchema?: {
15
+ type: string;
16
+ properties?: Record<string, any>;
17
+ };
18
+ }
19
+
20
+ export interface InputFormHandle {
21
+ submit: (startPaused: boolean) => void;
22
+ }
23
+
24
+ interface InputFormProps {
25
+ toolName: string;
26
+ onSubmit: (formData: Record<string, any>, startPaused: boolean) => void;
27
+ disabled?: boolean;
28
+ }
29
+
30
+ const InputForm = forwardRef<InputFormHandle, InputFormProps>(({ toolName, onSubmit, disabled }, ref) => {
31
+ const [toolInfo, setToolInfo] = useState<ToolInfo | null>(null);
32
+ const [formData, setFormData] = useState<Record<string, any>>({});
33
+ const [error, setError] = useState<string | null>(null);
34
+ const formRef = useRef<HTMLFormElement>(null);
35
+ const pendingStartPausedRef = useRef<boolean>(false);
36
+
37
+ useEffect(() => {
38
+ // Fetch tool information
39
+ fetch(`/api/tools/${toolName}`)
40
+ .then(res => res.json())
41
+ .then(data => {
42
+ if (data.error) {
43
+ setError(data.error);
44
+ return;
45
+ }
46
+ setToolInfo(data.tool);
47
+ // Initialize form data with default values
48
+ const defaults: Record<string, any> = {};
49
+ if (data.tool.inputSchema.properties) {
50
+ Object.entries(data.tool.inputSchema.properties).forEach(([key, prop]: [string, any]) => {
51
+ if (prop.type === 'string') {
52
+ defaults[key] = '';
53
+ } else if (prop.type === 'number') {
54
+ defaults[key] = 0;
55
+ } else if (prop.type === 'boolean') {
56
+ defaults[key] = false;
57
+ } else if (prop.type === 'array') {
58
+ defaults[key] = [];
59
+ } else if (prop.type === 'object') {
60
+ defaults[key] = {};
61
+ }
62
+ });
63
+ }
64
+ setFormData(defaults);
65
+ })
66
+ .catch(err => {
67
+ setError(err.message);
68
+ });
69
+ }, [toolName]);
70
+
71
+ // Expose submit method to parent via ref
72
+ useImperativeHandle(ref, () => ({
73
+ submit: (startPaused: boolean) => {
74
+ pendingStartPausedRef.current = startPaused;
75
+ if (formRef.current) {
76
+ formRef.current.requestSubmit();
77
+ } else {
78
+ console.warn('[InputForm] formRef.current is null, cannot submit form');
79
+ }
80
+ },
81
+ }));
82
+
83
+ const handleInputChange = (key: string, value: any) => {
84
+ setFormData(prev => ({
85
+ ...prev,
86
+ [key]: value,
87
+ }));
88
+ };
89
+
90
+ const handleSubmit = (e: React.FormEvent) => {
91
+ e.preventDefault();
92
+ const startPaused = pendingStartPausedRef.current;
93
+ pendingStartPausedRef.current = false;
94
+
95
+ // Validate required fields
96
+ if (toolInfo?.inputSchema.required) {
97
+ for (const field of toolInfo.inputSchema.required) {
98
+ if (!formData[field] || (typeof formData[field] === 'string' && formData[field].trim() === '')) {
99
+ setError(`Field "${field}" is required`);
100
+ return;
101
+ }
102
+ }
103
+ }
104
+
105
+ setError(null);
106
+ onSubmit(formData, startPaused);
107
+ };
108
+
109
+ const renderInputField = (key: string, prop: any) => {
110
+ const value = formData[key];
111
+ const isRequired = toolInfo?.inputSchema.required?.includes(key);
112
+
113
+ switch (prop.type) {
114
+ case 'string':
115
+ if (prop.format === 'multiline' || (typeof value === 'string' && value.includes('\n'))) {
116
+ return (
117
+ <div key={key} className={styles.field}>
118
+ <label htmlFor={key} className={styles.label}>
119
+ {key}
120
+ {isRequired && <span className={styles.required}>*</span>}
121
+ </label>
122
+ <textarea
123
+ id={key}
124
+ value={typeof value === 'string' ? value : JSON.stringify(value)}
125
+ onChange={e => {
126
+ try {
127
+ const parsed = JSON.parse(e.target.value);
128
+ handleInputChange(key, parsed);
129
+ } catch {
130
+ handleInputChange(key, e.target.value);
131
+ }
132
+ }}
133
+ className={styles.textarea}
134
+ placeholder={prop.description || `Enter ${key}`}
135
+ rows={3}
136
+ disabled={disabled}
137
+ />
138
+ {prop.description && (
139
+ <div className={styles.hint}>{prop.description}</div>
140
+ )}
141
+ </div>
142
+ );
143
+ }
144
+ return (
145
+ <div key={key} className={styles.field}>
146
+ <label htmlFor={key} className={styles.label}>
147
+ {key}
148
+ {isRequired && <span className={styles.required}>*</span>}
149
+ </label>
150
+ <input
151
+ id={key}
152
+ type="text"
153
+ value={typeof value === 'string' ? value : JSON.stringify(value)}
154
+ onChange={e => {
155
+ try {
156
+ const parsed = JSON.parse(e.target.value);
157
+ handleInputChange(key, parsed);
158
+ } catch {
159
+ handleInputChange(key, e.target.value);
160
+ }
161
+ }}
162
+ className={styles.input}
163
+ placeholder={prop.description || `Enter ${key}`}
164
+ disabled={disabled}
165
+ />
166
+ {prop.description && (
167
+ <div className={styles.hint}>{prop.description}</div>
168
+ )}
169
+ </div>
170
+ );
171
+ case 'number':
172
+ return (
173
+ <div key={key} className={styles.field}>
174
+ <label htmlFor={key} className={styles.label}>
175
+ {key}
176
+ {isRequired && <span className={styles.required}>*</span>}
177
+ </label>
178
+ <input
179
+ id={key}
180
+ type="number"
181
+ value={typeof value === 'number' ? value : ''}
182
+ onChange={e => handleInputChange(key, e.target.value ? Number(e.target.value) : 0)}
183
+ className={styles.input}
184
+ placeholder={prop.description || `Enter ${key}`}
185
+ disabled={disabled}
186
+ />
187
+ {prop.description && (
188
+ <div className={styles.hint}>{prop.description}</div>
189
+ )}
190
+ </div>
191
+ );
192
+ case 'boolean':
193
+ return (
194
+ <div key={key} className={styles.field}>
195
+ <label htmlFor={key} className={styles.checkboxLabel}>
196
+ <input
197
+ id={key}
198
+ type="checkbox"
199
+ checked={value === true}
200
+ onChange={e => handleInputChange(key, e.target.checked)}
201
+ className={styles.checkbox}
202
+ disabled={disabled}
203
+ />
204
+ {key}
205
+ {isRequired && <span className={styles.required}>*</span>}
206
+ </label>
207
+ {prop.description && (
208
+ <div className={styles.hint}>{prop.description}</div>
209
+ )}
210
+ </div>
211
+ );
212
+ default:
213
+ // For complex types (object, array), use textarea with JSON
214
+ return (
215
+ <div key={key} className={styles.field}>
216
+ <label htmlFor={key} className={styles.label}>
217
+ {key}
218
+ {isRequired && <span className={styles.required}>*</span>}
219
+ </label>
220
+ <textarea
221
+ id={key}
222
+ value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
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
+ disabled={disabled}
235
+ />
236
+ {prop.description && (
237
+ <div className={styles.hint}>{prop.description}</div>
238
+ )}
239
+ </div>
240
+ );
241
+ }
242
+ };
243
+
244
+ if (!toolInfo) {
245
+ return <div className={styles.loading}>Loading tool information...</div>;
246
+ }
247
+
248
+ if (error) {
249
+ return (
250
+ <div className={styles.error}>
251
+ <strong>Error:</strong> {error}
252
+ </div>
253
+ );
254
+ }
255
+
256
+ return (
257
+ <form ref={formRef} onSubmit={handleSubmit} className={styles.form}>
258
+ <div className={styles.inputs}>
259
+ {toolInfo.inputSchema.properties &&
260
+ Object.entries(toolInfo.inputSchema.properties).map(([key, prop]) =>
261
+ renderInputField(key, prop)
262
+ )}
263
+ </div>
264
+ </form>
265
+ );
266
+ });
267
+
268
+ InputForm.displayName = 'InputForm';
269
+
270
+ export default InputForm;
271
+
@@ -0,0 +1,118 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ flex-shrink: 0;
5
+ border-bottom: 1px solid #e0e0e0;
6
+ margin-bottom: 0;
7
+ }
8
+
9
+ .title {
10
+ padding: 1rem 1.5rem;
11
+ font-size: 1.1rem;
12
+ font-weight: 600;
13
+ color: #333;
14
+ border-bottom: 1px solid #e0e0e0;
15
+ background-color: #fafafa;
16
+ margin: 0;
17
+ }
18
+
19
+ .configInfo {
20
+ padding: 1rem 1.5rem;
21
+ border-bottom: 1px solid #f0f0f0;
22
+ }
23
+
24
+ .configName {
25
+ font-weight: 600;
26
+ color: #333;
27
+ font-size: 0.95rem;
28
+ margin-bottom: 0.25rem;
29
+ }
30
+
31
+ .configVersion {
32
+ font-size: 0.85rem;
33
+ color: #666;
34
+ }
35
+
36
+ .configDescription {
37
+ font-size: 0.85rem;
38
+ color: #666;
39
+ margin-top: 0.25rem;
40
+ font-style: italic;
41
+ }
42
+
43
+ .servers {
44
+ max-height: 300px;
45
+ overflow-y: auto;
46
+ }
47
+
48
+ .server {
49
+ padding: 1rem 1.5rem;
50
+ border-bottom: 1px solid #f0f0f0;
51
+ }
52
+
53
+ .serverName {
54
+ font-weight: 600;
55
+ color: #2196f3;
56
+ font-size: 0.9rem;
57
+ margin-bottom: 0.5rem;
58
+ }
59
+
60
+ .serverType,
61
+ .serverCommand,
62
+ .serverArgs,
63
+ .serverCwd,
64
+ .serverUrl,
65
+ .serverHeaders {
66
+ margin-bottom: 0.5rem;
67
+ font-size: 0.85rem;
68
+ }
69
+
70
+ .serverType:last-child,
71
+ .serverCommand:last-child,
72
+ .serverArgs:last-child,
73
+ .serverCwd:last-child,
74
+ .serverUrl:last-child,
75
+ .serverHeaders:last-child {
76
+ margin-bottom: 0;
77
+ }
78
+
79
+ .server:last-child {
80
+ border-bottom: none;
81
+ }
82
+
83
+ .label {
84
+ color: #666;
85
+ margin-right: 0.5rem;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .code {
90
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
91
+ font-size: 0.85rem;
92
+ color: #333;
93
+ word-break: break-all;
94
+ }
95
+
96
+ .headers {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.25rem;
100
+ margin-top: 0.25rem;
101
+ margin-left: 0.5rem;
102
+ }
103
+
104
+ .header {
105
+ margin: 0;
106
+ }
107
+
108
+ .loading,
109
+ .error {
110
+ padding: 0.5rem;
111
+ font-size: 0.85rem;
112
+ color: #666;
113
+ }
114
+
115
+ .error {
116
+ color: #d32f2f;
117
+ }
118
+
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import styles from './ServerDetails.module.css';
4
+
5
+
6
+ interface ServerInfo {
7
+ name: string;
8
+ type: string;
9
+ command?: string;
10
+ args?: string[];
11
+ cwd?: string;
12
+ url?: string;
13
+ headers?: Record<string, string>;
14
+ }
15
+
16
+ interface ServerDetailsProps {
17
+ config: {
18
+ name: string;
19
+ version: string;
20
+ description?: string;
21
+ servers: ServerInfo[];
22
+ };
23
+ }
24
+
25
+ // Server config section
26
+ export function ServerConfig({ config }: ServerDetailsProps) {
27
+ if (!config || !config.name) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <div className={styles.container}>
33
+ <h2 className={styles.title}>Server</h2>
34
+ <div className={styles.configInfo}>
35
+ <div className={styles.configName}>{config.name}</div>
36
+ {config.version && (
37
+ <div className={styles.configVersion}>v{config.version}</div>
38
+ )}
39
+ {config.description && (
40
+ <div className={styles.configDescription}>{config.description}</div>
41
+ )}
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ // MCP Servers section
48
+ export function McpServers({ config }: ServerDetailsProps) {
49
+ if (!config || !config.servers || config.servers.length === 0) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <div className={styles.container}>
55
+ <h2 className={styles.title}>MCP Servers</h2>
56
+ <div className={styles.servers}>
57
+ {config.servers.map((server, index) => (
58
+ <div key={index} className={styles.server}>
59
+ <div className={styles.serverName}>{server.name}</div>
60
+ <div className={styles.serverType}>
61
+ <span className={styles.label}>Type:</span>
62
+ <code className={styles.code}>{server.type}</code>
63
+ </div>
64
+ {server.command && (
65
+ <div className={styles.serverCommand}>
66
+ <span className={styles.label}>Command:</span>
67
+ <code className={styles.code}>{server.command}</code>
68
+ </div>
69
+ )}
70
+ {server.args && server.args.length > 0 && (
71
+ <div className={styles.serverArgs}>
72
+ <span className={styles.label}>Args:</span>
73
+ <code className={styles.code}>{server.args.join(' ')}</code>
74
+ </div>
75
+ )}
76
+ {server.cwd && (
77
+ <div className={styles.serverCwd}>
78
+ <span className={styles.label}>CWD:</span>
79
+ <code className={styles.code}>{server.cwd}</code>
80
+ </div>
81
+ )}
82
+ {server.url && (
83
+ <div className={styles.serverUrl}>
84
+ <span className={styles.label}>URL:</span>
85
+ <code className={styles.code}>{server.url}</code>
86
+ </div>
87
+ )}
88
+ {server.headers && Object.keys(server.headers).length > 0 && (
89
+ <div className={styles.serverHeaders}>
90
+ <span className={styles.label}>Headers:</span>
91
+ <div className={styles.headers}>
92
+ {Object.entries(server.headers).map(([key, value]) => (
93
+ <div key={key} className={styles.header}>
94
+ <code className={styles.code}>{key}: {value}</code>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </div>
99
+ )}
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // Legacy default export for backwards compatibility
108
+ export default function ServerDetails({ config }: ServerDetailsProps) {
109
+ return (
110
+ <>
111
+ <ServerConfig config={config} />
112
+ <McpServers config={config} />
113
+ </>
114
+ );
115
+ }
116
+
@@ -0,0 +1,177 @@
1
+ .container {
2
+ width: 100%;
3
+ display: flex;
4
+ flex-direction: column;
5
+ border: 1px solid #e0e0e0;
6
+ border-radius: 8px;
7
+ background: white;
8
+ }
9
+
10
+ .title {
11
+ padding: 12px 16px;
12
+ margin: 0;
13
+ font-size: 16px;
14
+ font-weight: 600;
15
+ border-bottom: 1px solid #e0e0e0;
16
+ background: #f5f5f5;
17
+ border-radius: 8px 8px 0 0;
18
+ }
19
+
20
+ .empty {
21
+ padding: 24px;
22
+ text-align: center;
23
+ color: #999;
24
+ font-style: italic;
25
+ }
26
+
27
+ .metrics {
28
+ display: grid;
29
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
30
+ gap: 12px;
31
+ padding: 16px;
32
+ }
33
+
34
+ .metricCard {
35
+ display: flex;
36
+ flex-direction: column;
37
+ padding: 12px;
38
+ background: #f9f9f9;
39
+ border-radius: 6px;
40
+ border: 1px solid #e0e0e0;
41
+ }
42
+
43
+ .metricLabel {
44
+ font-size: 11px;
45
+ color: #666;
46
+ text-transform: uppercase;
47
+ letter-spacing: 0.5px;
48
+ margin-bottom: 4px;
49
+ }
50
+
51
+ .metricValue {
52
+ font-size: 20px;
53
+ font-weight: 600;
54
+ color: #2196f3;
55
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
56
+ }
57
+
58
+ .metricValue.error {
59
+ color: #ef5350;
60
+ }
61
+
62
+ .section {
63
+ padding: 16px;
64
+ border-top: 1px solid #e0e0e0;
65
+ }
66
+
67
+ .sectionTitle {
68
+ margin: 0 0 12px 0;
69
+ font-size: 14px;
70
+ font-weight: 600;
71
+ color: #333;
72
+ }
73
+
74
+ .nodeTypeList {
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 12px;
78
+ }
79
+
80
+ .nodeTypeItem {
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 6px;
84
+ }
85
+
86
+ .nodeTypeHeader {
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ }
91
+
92
+ .nodeTypeName {
93
+ font-weight: 600;
94
+ font-size: 13px;
95
+ color: #333;
96
+ text-transform: capitalize;
97
+ }
98
+
99
+ .nodeTypeCount {
100
+ font-size: 11px;
101
+ color: #666;
102
+ }
103
+
104
+ .progressBar {
105
+ height: 8px;
106
+ background: #e0e0e0;
107
+ border-radius: 4px;
108
+ overflow: hidden;
109
+ }
110
+
111
+ .progressFill {
112
+ height: 100%;
113
+ background: linear-gradient(90deg, #2196f3, #42a5f5);
114
+ transition: width 0.3s ease;
115
+ }
116
+
117
+ .nodeTypeStats {
118
+ display: flex;
119
+ gap: 12px;
120
+ font-size: 11px;
121
+ color: #666;
122
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
123
+ }
124
+
125
+ .slowNodesList {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 8px;
129
+ }
130
+
131
+ .slowNodeItem {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 12px;
135
+ padding: 8px;
136
+ background: #f9f9f9;
137
+ border-radius: 4px;
138
+ position: relative;
139
+ }
140
+
141
+ .slowNodeId {
142
+ flex: 1;
143
+ font-size: 12px;
144
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
145
+ color: #333;
146
+ min-width: 0;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ white-space: nowrap;
150
+ }
151
+
152
+ .slowNodeDuration {
153
+ font-size: 12px;
154
+ font-weight: 600;
155
+ color: #2196f3;
156
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .slowNodeBar {
161
+ position: absolute;
162
+ bottom: 0;
163
+ left: 0;
164
+ right: 0;
165
+ height: 2px;
166
+ background: transparent;
167
+ overflow: hidden;
168
+ border-radius: 0 0 4px 4px;
169
+ }
170
+
171
+ .slowNodeFill {
172
+ height: 100%;
173
+ background: #2196f3;
174
+ opacity: 0.3;
175
+ transition: width 0.3s ease;
176
+ }
177
+