nitrostack 1.0.1 → 1.0.2
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/CHANGELOG.md +15 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +123 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +85 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +41 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { ToolCard } from '@/components/ToolCard';
|
|
7
|
+
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
8
|
+
import type { Tool } from '@/lib/types';
|
|
9
|
+
import { Wrench, RefreshCw, X, Play, AlertCircle } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
export default function ToolsPage() {
|
|
12
|
+
const { tools, setTools, loading, setLoading, connection, setConnection, jwtToken, apiKey } = useStudioStore();
|
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
14
|
+
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
|
15
|
+
const [toolArgs, setToolArgs] = useState<Record<string, any>>({});
|
|
16
|
+
const [toolResult, setToolResult] = useState<any>(null);
|
|
17
|
+
const [executingTool, setExecutingTool] = useState(false);
|
|
18
|
+
|
|
19
|
+
// Initialize MCP, load tools and check connection on mount
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const init = async () => {
|
|
22
|
+
await api.initialize();
|
|
23
|
+
await loadTools();
|
|
24
|
+
await checkConnection();
|
|
25
|
+
};
|
|
26
|
+
init();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const checkConnection = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const health = await api.checkConnection();
|
|
32
|
+
setConnection({
|
|
33
|
+
connected: health.connected,
|
|
34
|
+
status: health.connected ? 'connected' : 'disconnected',
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
setConnection({ connected: false, status: 'disconnected' });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const loadTools = async () => {
|
|
42
|
+
setLoading('tools', true);
|
|
43
|
+
try {
|
|
44
|
+
const data = await api.getTools();
|
|
45
|
+
setTools(data.tools || []);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Failed to load tools:', error);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading('tools', false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleExecuteTool = (tool: Tool) => {
|
|
54
|
+
setSelectedTool(tool);
|
|
55
|
+
setToolArgs({});
|
|
56
|
+
setToolResult(null);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleSubmitTool = async (e: React.FormEvent) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (!selectedTool) return;
|
|
62
|
+
|
|
63
|
+
setExecutingTool(true);
|
|
64
|
+
setToolResult(null);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Pass JWT token and API key if available
|
|
68
|
+
const result = await api.callTool(selectedTool.name, toolArgs, jwtToken || undefined, apiKey || undefined);
|
|
69
|
+
setToolResult(result);
|
|
70
|
+
|
|
71
|
+
// Extract JWT token from ANY tool response (not just 'login')
|
|
72
|
+
// Check if result contains a token field at any level
|
|
73
|
+
if (result.content) {
|
|
74
|
+
try {
|
|
75
|
+
const content = result.content[0]?.text;
|
|
76
|
+
if (content) {
|
|
77
|
+
const parsed = JSON.parse(content);
|
|
78
|
+
// Check for token in multiple possible locations
|
|
79
|
+
const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
|
|
80
|
+
if (token) {
|
|
81
|
+
console.log('🔐 Token received from tool, saving to global state');
|
|
82
|
+
useStudioStore.getState().setJwtToken(token);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Ignore parsing errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Tool execution failed:', error);
|
|
91
|
+
setToolResult({ error: 'Tool execution failed' });
|
|
92
|
+
} finally {
|
|
93
|
+
setExecutingTool(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const filteredTools = tools.filter((tool) =>
|
|
98
|
+
tool.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<div className="min-h-screen bg-background p-8">
|
|
104
|
+
{/* Header */}
|
|
105
|
+
<div className="mb-8">
|
|
106
|
+
<div className="flex items-center justify-between mb-6">
|
|
107
|
+
<div className="flex items-center gap-3">
|
|
108
|
+
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center">
|
|
109
|
+
<Wrench className="w-6 h-6 text-black" />
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-3xl font-bold text-foreground">Tools</h1>
|
|
113
|
+
<p className="text-muted-foreground mt-1">
|
|
114
|
+
Browse and execute MCP tools
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<button onClick={loadTools} className="btn btn-primary gap-2">
|
|
119
|
+
<RefreshCw className="w-4 h-4" />
|
|
120
|
+
Refresh
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Search */}
|
|
125
|
+
<input
|
|
126
|
+
type="text"
|
|
127
|
+
placeholder="Search tools..."
|
|
128
|
+
value={searchQuery}
|
|
129
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
130
|
+
className="input"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Tools Grid */}
|
|
135
|
+
{loading.tools ? (
|
|
136
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
137
|
+
{[1, 2, 3].map((i) => (
|
|
138
|
+
<div key={i} className="card skeleton h-64"></div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
) : filteredTools.length === 0 ? (
|
|
142
|
+
<div className="empty-state">
|
|
143
|
+
<AlertCircle className="empty-state-icon" />
|
|
144
|
+
<p className="empty-state-title">
|
|
145
|
+
{searchQuery ? 'No tools found matching your search' : 'No tools available'}
|
|
146
|
+
</p>
|
|
147
|
+
<p className="empty-state-description">
|
|
148
|
+
{searchQuery ? 'Try a different search term' : 'No MCP tools have been registered'}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
) : (
|
|
152
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
153
|
+
{filteredTools.map((tool) => (
|
|
154
|
+
<ToolCard key={tool.name} tool={tool} onExecute={handleExecuteTool} />
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Tool Executor Modal */}
|
|
160
|
+
{selectedTool && (
|
|
161
|
+
<div
|
|
162
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
|
163
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
164
|
+
onClick={() => setSelectedTool(null)}
|
|
165
|
+
>
|
|
166
|
+
<div
|
|
167
|
+
className="w-[700px] max-h-[90vh] overflow-auto rounded-2xl border border-border p-6 bg-card shadow-2xl animate-scale-in"
|
|
168
|
+
onClick={(e) => e.stopPropagation()}
|
|
169
|
+
>
|
|
170
|
+
<div className="flex items-center justify-between mb-4">
|
|
171
|
+
<div className="flex items-center gap-3">
|
|
172
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
173
|
+
<Wrench className="w-5 h-5 text-primary" />
|
|
174
|
+
</div>
|
|
175
|
+
<h2 className="text-xl font-bold text-foreground">{selectedTool.name}</h2>
|
|
176
|
+
</div>
|
|
177
|
+
<button
|
|
178
|
+
onClick={() => setSelectedTool(null)}
|
|
179
|
+
className="btn btn-ghost w-10 h-10 p-0"
|
|
180
|
+
>
|
|
181
|
+
<X className="w-5 h-5" />
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
186
|
+
{selectedTool.description || 'No description'}
|
|
187
|
+
</p>
|
|
188
|
+
|
|
189
|
+
<form onSubmit={handleSubmitTool}>
|
|
190
|
+
{/* Generate form inputs from schema */}
|
|
191
|
+
{selectedTool.inputSchema?.properties && typeof selectedTool.inputSchema.properties === 'object' && Object.keys(selectedTool.inputSchema.properties).length > 0 ? (
|
|
192
|
+
<>
|
|
193
|
+
{Object.entries(selectedTool.inputSchema.properties).map(([key, prop]: [string, any]) => {
|
|
194
|
+
const isRequired = selectedTool.inputSchema?.required?.includes(key);
|
|
195
|
+
|
|
196
|
+
// Handle different input types
|
|
197
|
+
if (prop.enum) {
|
|
198
|
+
// Enum/Select field
|
|
199
|
+
return (
|
|
200
|
+
<div key={key} className="mb-4">
|
|
201
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
202
|
+
{prop.title || key}
|
|
203
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
204
|
+
</label>
|
|
205
|
+
<select
|
|
206
|
+
className="input"
|
|
207
|
+
value={toolArgs[key] || prop.default || ''}
|
|
208
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })}
|
|
209
|
+
required={isRequired}
|
|
210
|
+
>
|
|
211
|
+
<option value="">Select...</option>
|
|
212
|
+
{prop.enum.map((val: any) => (
|
|
213
|
+
<option key={val} value={val}>
|
|
214
|
+
{val}
|
|
215
|
+
</option>
|
|
216
|
+
))}
|
|
217
|
+
</select>
|
|
218
|
+
{prop.description && (
|
|
219
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
} else if (prop.type === 'boolean') {
|
|
224
|
+
// Checkbox field
|
|
225
|
+
return (
|
|
226
|
+
<div key={key} className="mb-4">
|
|
227
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
228
|
+
<input
|
|
229
|
+
type="checkbox"
|
|
230
|
+
className="w-4 h-4"
|
|
231
|
+
checked={toolArgs[key] || false}
|
|
232
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })}
|
|
233
|
+
/>
|
|
234
|
+
<span className="text-sm font-medium text-foreground">
|
|
235
|
+
{prop.title || key}
|
|
236
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
237
|
+
</span>
|
|
238
|
+
</label>
|
|
239
|
+
{prop.description && (
|
|
240
|
+
<p className="text-xs text-muted-foreground mt-1 ml-6">{prop.description}</p>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// Text/Number field
|
|
246
|
+
return (
|
|
247
|
+
<div key={key} className="mb-4">
|
|
248
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
249
|
+
{prop.title || key}
|
|
250
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
251
|
+
</label>
|
|
252
|
+
<input
|
|
253
|
+
type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'}
|
|
254
|
+
className="input"
|
|
255
|
+
value={toolArgs[key] || prop.default || ''}
|
|
256
|
+
onChange={(e) => {
|
|
257
|
+
const value = prop.type === 'number' || prop.type === 'integer'
|
|
258
|
+
? (e.target.value ? Number(e.target.value) : '')
|
|
259
|
+
: e.target.value;
|
|
260
|
+
setToolArgs({ ...toolArgs, [key]: value });
|
|
261
|
+
}}
|
|
262
|
+
required={isRequired}
|
|
263
|
+
placeholder={prop.description}
|
|
264
|
+
min={prop.minimum}
|
|
265
|
+
max={prop.maximum}
|
|
266
|
+
/>
|
|
267
|
+
{prop.description && (
|
|
268
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
})}
|
|
274
|
+
</>
|
|
275
|
+
) : (
|
|
276
|
+
<div className="mb-4 p-4 bg-muted/30 rounded-lg border border-border">
|
|
277
|
+
<p className="text-sm text-muted-foreground">No input required</p>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<button
|
|
282
|
+
type="submit"
|
|
283
|
+
className="btn btn-primary w-full gap-2"
|
|
284
|
+
disabled={executingTool}
|
|
285
|
+
>
|
|
286
|
+
<Play className="w-4 h-4" />
|
|
287
|
+
{executingTool ? 'Executing...' : 'Execute Tool'}
|
|
288
|
+
</button>
|
|
289
|
+
</form>
|
|
290
|
+
|
|
291
|
+
{/* Result */}
|
|
292
|
+
{toolResult && (
|
|
293
|
+
<div className="mt-6 space-y-4">
|
|
294
|
+
<div>
|
|
295
|
+
<h3 className="font-semibold text-foreground mb-3">Result:</h3>
|
|
296
|
+
<pre className="bg-muted/30 p-4 rounded-lg text-sm overflow-auto max-h-64 text-foreground font-mono border border-border">
|
|
297
|
+
{JSON.stringify(toolResult, null, 2)}
|
|
298
|
+
</pre>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Widget UI Rendering */}
|
|
302
|
+
{(selectedTool.widget || selectedTool.outputTemplate) && toolResult && (
|
|
303
|
+
<div>
|
|
304
|
+
<h3 className="font-semibold text-foreground mb-3">UI Widget:</h3>
|
|
305
|
+
<div className="border border-border rounded-lg overflow-hidden h-64 bg-background shadow-inner">
|
|
306
|
+
<WidgetRenderer
|
|
307
|
+
uri={selectedTool.widget?.route || selectedTool.outputTemplate}
|
|
308
|
+
data={(() => {
|
|
309
|
+
// Try to parse JSON from content[0].text, otherwise use raw result
|
|
310
|
+
if (toolResult.content?.[0]?.text) {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(toolResult.content[0].text);
|
|
313
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
314
|
+
if (parsed.success !== undefined && parsed.data !== undefined) {
|
|
315
|
+
return parsed.data;
|
|
316
|
+
}
|
|
317
|
+
return parsed;
|
|
318
|
+
} catch {
|
|
319
|
+
return { message: toolResult.content[0].text };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return toolResult;
|
|
323
|
+
})()}
|
|
324
|
+
className="w-full h-full"
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { ToolCard } from '@/components/ToolCard';
|
|
7
|
+
import { EnlargeModal } from '@/components/EnlargeModal';
|
|
8
|
+
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
9
|
+
import type { Tool } from '@/lib/types';
|
|
10
|
+
|
|
11
|
+
export default function ToolsPage() {
|
|
12
|
+
const { tools, setTools, loading, setLoading, connection, setConnection } = useStudioStore();
|
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
14
|
+
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
|
15
|
+
const [toolArgs, setToolArgs] = useState<Record<string, any>>({});
|
|
16
|
+
const [toolResult, setToolResult] = useState<any>(null);
|
|
17
|
+
const [executingTool, setExecutingTool] = useState(false);
|
|
18
|
+
|
|
19
|
+
// Initialize MCP, load tools and check connection on mount
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const init = async () => {
|
|
22
|
+
await api.initialize();
|
|
23
|
+
await loadTools();
|
|
24
|
+
await checkConnection();
|
|
25
|
+
};
|
|
26
|
+
init();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const checkConnection = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const health = await api.checkConnection();
|
|
32
|
+
setConnection({
|
|
33
|
+
connected: health.connected,
|
|
34
|
+
status: health.connected ? 'connected' : 'disconnected',
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
setConnection({ connected: false, status: 'disconnected' });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const loadTools = async () => {
|
|
42
|
+
setLoading('tools', true);
|
|
43
|
+
try {
|
|
44
|
+
const data = await api.getTools();
|
|
45
|
+
setTools(data.tools || []);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Failed to load tools:', error);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading('tools', false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleExecuteTool = (tool: Tool) => {
|
|
54
|
+
setSelectedTool(tool);
|
|
55
|
+
setToolArgs({});
|
|
56
|
+
setToolResult(null);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleSubmitTool = async (e: React.FormEvent) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (!selectedTool) return;
|
|
62
|
+
|
|
63
|
+
setExecutingTool(true);
|
|
64
|
+
setToolResult(null);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await api.callTool(selectedTool.name, toolArgs);
|
|
68
|
+
setToolResult(result);
|
|
69
|
+
|
|
70
|
+
// Check for JWT token in login response
|
|
71
|
+
if (selectedTool.name === 'login' && result.content) {
|
|
72
|
+
try {
|
|
73
|
+
const content = result.content[0]?.text;
|
|
74
|
+
if (content) {
|
|
75
|
+
const parsed = JSON.parse(content);
|
|
76
|
+
if (parsed.token) {
|
|
77
|
+
useStudioStore.getState().setJwtToken(parsed.token);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Ignore parsing errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Tool execution failed:', error);
|
|
86
|
+
setToolResult({ error: 'Tool execution failed' });
|
|
87
|
+
} finally {
|
|
88
|
+
setExecutingTool(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const filteredTools = tools.filter((tool) =>
|
|
93
|
+
tool.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<div className="min-h-screen bg-dark-bg p-8">
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<div className="mb-8">
|
|
101
|
+
<div className="flex items-center justify-between mb-4">
|
|
102
|
+
<div>
|
|
103
|
+
<h1 className="text-3xl font-bold">⚡ Tools</h1>
|
|
104
|
+
<p className="text-text-secondary mt-1">
|
|
105
|
+
Browse and execute MCP tools
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<button onClick={loadTools} className="btn btn-primary">
|
|
109
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
110
|
+
<path
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
strokeLinejoin="round"
|
|
113
|
+
strokeWidth={2}
|
|
114
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
115
|
+
/>
|
|
116
|
+
</svg>
|
|
117
|
+
Refresh
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Search */}
|
|
122
|
+
<input
|
|
123
|
+
type="text"
|
|
124
|
+
placeholder="Search tools..."
|
|
125
|
+
value={searchQuery}
|
|
126
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
127
|
+
className="input"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Tools Grid */}
|
|
132
|
+
{loading.tools ? (
|
|
133
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
134
|
+
{[1, 2, 3].map((i) => (
|
|
135
|
+
<div key={i} className="card skeleton h-64"></div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
) : filteredTools.length === 0 ? (
|
|
139
|
+
<div className="text-center py-16">
|
|
140
|
+
<p className="text-text-secondary text-lg">
|
|
141
|
+
{searchQuery ? 'No tools found matching your search' : 'No tools available'}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
) : (
|
|
145
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
146
|
+
{filteredTools.map((tool) => (
|
|
147
|
+
<ToolCard key={tool.name} tool={tool} onExecute={handleExecuteTool} />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{/* Tool Executor Modal */}
|
|
153
|
+
{selectedTool && (
|
|
154
|
+
<div
|
|
155
|
+
className="fixed inset-0 z-40 flex items-center justify-center"
|
|
156
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.8)' }}
|
|
157
|
+
onClick={() => setSelectedTool(null)}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
className="w-[700px] max-h-[90vh] overflow-auto rounded-2xl border p-6"
|
|
161
|
+
style={{
|
|
162
|
+
backgroundColor: '#ffffff',
|
|
163
|
+
borderColor: '#e5e7eb'
|
|
164
|
+
}}
|
|
165
|
+
onClick={(e) => e.stopPropagation()}
|
|
166
|
+
>
|
|
167
|
+
<div className="flex items-center justify-between mb-4">
|
|
168
|
+
<h2 className="text-xl font-bold">{selectedTool.name}</h2>
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => setSelectedTool(null)}
|
|
171
|
+
className="text-text-secondary hover:text-text-primary"
|
|
172
|
+
>
|
|
173
|
+
✕
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<p className="text-sm text-text-secondary mb-6">
|
|
178
|
+
{selectedTool.description || 'No description'}
|
|
179
|
+
</p>
|
|
180
|
+
|
|
181
|
+
<form onSubmit={handleSubmitTool}>
|
|
182
|
+
{/* Debug Info */}
|
|
183
|
+
<div className="mb-4 p-4 rounded-lg" style={{ backgroundColor: '#fef3c7', border: '1px solid #fbbf24' }}>
|
|
184
|
+
<p className="text-xs font-mono" style={{ color: '#92400e' }}>
|
|
185
|
+
DEBUG: Has inputSchema: {selectedTool.inputSchema ? 'YES' : 'NO'}<br/>
|
|
186
|
+
Has properties: {selectedTool.inputSchema?.properties ? 'YES' : 'NO'}<br/>
|
|
187
|
+
Properties type: {typeof selectedTool.inputSchema?.properties}<br/>
|
|
188
|
+
Properties keys: {selectedTool.inputSchema?.properties ? JSON.stringify(Object.keys(selectedTool.inputSchema.properties)) : 'NONE'}<br/>
|
|
189
|
+
Raw schema: {JSON.stringify(selectedTool.inputSchema, null, 2).substring(0, 200)}...
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Generate form inputs from schema */}
|
|
194
|
+
{selectedTool.inputSchema?.properties && typeof selectedTool.inputSchema.properties === 'object' && Object.keys(selectedTool.inputSchema.properties).length > 0 ? (
|
|
195
|
+
<>
|
|
196
|
+
<div className="mb-4 p-3 rounded-lg" style={{ backgroundColor: '#f0fdf4' }}>
|
|
197
|
+
<p className="text-sm font-medium" style={{ color: '#166534' }}>✓ Input Fields Detected:</p>
|
|
198
|
+
</div>
|
|
199
|
+
{Object.entries(selectedTool.inputSchema.properties).map(([key, prop]: [string, any]) => {
|
|
200
|
+
const isRequired = selectedTool.inputSchema?.required?.includes(key);
|
|
201
|
+
|
|
202
|
+
console.log('Rendering input field:', { key, prop, isRequired });
|
|
203
|
+
|
|
204
|
+
// Handle different input types
|
|
205
|
+
if (prop.enum) {
|
|
206
|
+
// Enum/Select field
|
|
207
|
+
return (
|
|
208
|
+
<div key={key} className="mb-4">
|
|
209
|
+
<label className="block text-sm font-medium mb-2">
|
|
210
|
+
{prop.title || key}
|
|
211
|
+
{isRequired && <span className="text-red-500 ml-1">*</span>}
|
|
212
|
+
</label>
|
|
213
|
+
<select
|
|
214
|
+
className="input"
|
|
215
|
+
value={toolArgs[key] || prop.default || ''}
|
|
216
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })}
|
|
217
|
+
required={isRequired}
|
|
218
|
+
>
|
|
219
|
+
<option value="">Select...</option>
|
|
220
|
+
{prop.enum.map((val: any) => (
|
|
221
|
+
<option key={val} value={val}>
|
|
222
|
+
{val}
|
|
223
|
+
</option>
|
|
224
|
+
))}
|
|
225
|
+
</select>
|
|
226
|
+
{prop.description && (
|
|
227
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
} else if (prop.type === 'boolean') {
|
|
232
|
+
// Checkbox field
|
|
233
|
+
return (
|
|
234
|
+
<div key={key} className="mb-4">
|
|
235
|
+
<label className="flex items-center gap-2">
|
|
236
|
+
<input
|
|
237
|
+
type="checkbox"
|
|
238
|
+
checked={toolArgs[key] || false}
|
|
239
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })}
|
|
240
|
+
/>
|
|
241
|
+
<span className="text-sm font-medium">
|
|
242
|
+
{prop.title || key}
|
|
243
|
+
{isRequired && <span className="text-red-500 ml-1">*</span>}
|
|
244
|
+
</span>
|
|
245
|
+
</label>
|
|
246
|
+
{prop.description && (
|
|
247
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
// Text/Number field
|
|
253
|
+
return (
|
|
254
|
+
<div key={key} className="mb-4">
|
|
255
|
+
<label className="block text-sm font-medium mb-2">
|
|
256
|
+
{prop.title || key}
|
|
257
|
+
{isRequired && <span className="text-red-500 ml-1">*</span>}
|
|
258
|
+
</label>
|
|
259
|
+
<input
|
|
260
|
+
type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'}
|
|
261
|
+
className="input"
|
|
262
|
+
value={toolArgs[key] || prop.default || ''}
|
|
263
|
+
onChange={(e) => {
|
|
264
|
+
const value = prop.type === 'number' || prop.type === 'integer'
|
|
265
|
+
? (e.target.value ? Number(e.target.value) : '')
|
|
266
|
+
: e.target.value;
|
|
267
|
+
setToolArgs({ ...toolArgs, [key]: value });
|
|
268
|
+
}}
|
|
269
|
+
required={isRequired}
|
|
270
|
+
placeholder={prop.description}
|
|
271
|
+
min={prop.minimum}
|
|
272
|
+
max={prop.maximum}
|
|
273
|
+
/>
|
|
274
|
+
{prop.description && (
|
|
275
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
})}
|
|
281
|
+
</>
|
|
282
|
+
) : (
|
|
283
|
+
<p className="text-sm text-muted-foreground mb-4">No input required</p>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<button
|
|
287
|
+
type="submit"
|
|
288
|
+
className="btn btn-primary w-full"
|
|
289
|
+
disabled={executingTool}
|
|
290
|
+
>
|
|
291
|
+
{executingTool ? 'Executing...' : 'Execute Tool'}
|
|
292
|
+
</button>
|
|
293
|
+
</form>
|
|
294
|
+
|
|
295
|
+
{/* Result */}
|
|
296
|
+
{toolResult && (
|
|
297
|
+
<div className="mt-6 space-y-4">
|
|
298
|
+
<div>
|
|
299
|
+
<h3 className="font-medium mb-2">Result:</h3>
|
|
300
|
+
<pre className="bg-muted p-4 rounded-lg text-sm overflow-auto max-h-64 text-foreground">
|
|
301
|
+
{JSON.stringify(toolResult, null, 2)}
|
|
302
|
+
</pre>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Widget UI Rendering */}
|
|
306
|
+
{(selectedTool.widget || selectedTool.outputTemplate) && toolResult && (
|
|
307
|
+
<div>
|
|
308
|
+
<h3 className="font-medium mb-2">UI Widget:</h3>
|
|
309
|
+
<div className="border border-border rounded-lg overflow-hidden h-64">
|
|
310
|
+
<WidgetRenderer
|
|
311
|
+
uri={selectedTool.widget?.route || selectedTool.outputTemplate}
|
|
312
|
+
data={(() => {
|
|
313
|
+
// Try to parse JSON from content[0].text, otherwise use raw result
|
|
314
|
+
if (toolResult.content?.[0]?.text) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(toolResult.content[0].text);
|
|
317
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
318
|
+
// Check if it has the interceptor's structure: { success, data, metadata }
|
|
319
|
+
if (parsed.success !== undefined && parsed.data !== undefined) {
|
|
320
|
+
return parsed.data; // Return the unwrapped data
|
|
321
|
+
}
|
|
322
|
+
return parsed;
|
|
323
|
+
} catch {
|
|
324
|
+
// If parsing fails, return the text wrapped in an object
|
|
325
|
+
return { message: toolResult.content[0].text };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return toolResult;
|
|
329
|
+
})()}
|
|
330
|
+
className="w-full h-full"
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<EnlargeModal />
|
|
343
|
+
</>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|