nitrostack 1.0.65 → 1.0.66
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/package.json +2 -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 +250 -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 +109 -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 +175 -0
- package/src/studio/app/auth/page.tsx +560 -0
- package/src/studio/app/chat/page.tsx +1133 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +486 -0
- package/src/studio/app/health/page.tsx +179 -0
- package/src/studio/app/layout.tsx +68 -0
- package/src/studio/app/logs/page.tsx +279 -0
- package/src/studio/app/page.tsx +351 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +209 -0
- package/src/studio/app/prompts/page.tsx +230 -0
- package/src/studio/app/resources/page.tsx +315 -0
- package/src/studio/app/settings/page.tsx +199 -0
- package/src/studio/branding.md +807 -0
- package/src/studio/components/EnlargeModal.tsx +138 -0
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +295 -0
- package/src/studio/components/ToolCard.tsx +139 -0
- package/src/studio/components/WidgetRenderer.tsx +346 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/http-client-transport.ts +222 -0
- package/src/studio/lib/llm-service.ts +480 -0
- package/src/studio/lib/log-manager.ts +76 -0
- package/src/studio/lib/mcp-client.ts +258 -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 +38 -0
- package/src/studio/package.json +35 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/public/nitrocloud.png +0 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +42 -0
|
@@ -0,0 +1,351 @@
|
|
|
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, oauthState } = 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
|
+
// Get effective token - check both jwtToken and OAuth token
|
|
20
|
+
const effectiveToken = jwtToken || oauthState?.currentToken;
|
|
21
|
+
|
|
22
|
+
// Initialize MCP, load tools and check connection on mount
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const init = async () => {
|
|
25
|
+
await api.initialize();
|
|
26
|
+
await loadTools();
|
|
27
|
+
await checkConnection();
|
|
28
|
+
};
|
|
29
|
+
init();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const checkConnection = async () => {
|
|
33
|
+
try {
|
|
34
|
+
const health = await api.checkConnection();
|
|
35
|
+
setConnection({
|
|
36
|
+
connected: health.connected,
|
|
37
|
+
status: health.connected ? 'connected' : 'disconnected',
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
setConnection({ connected: false, status: 'disconnected' });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const loadTools = async () => {
|
|
45
|
+
setLoading('tools', true);
|
|
46
|
+
try {
|
|
47
|
+
const data = await api.getTools();
|
|
48
|
+
setTools(data.tools || []);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to load tools:', error);
|
|
51
|
+
} finally {
|
|
52
|
+
setLoading('tools', false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleExecuteTool = (tool: Tool) => {
|
|
57
|
+
setSelectedTool(tool);
|
|
58
|
+
setToolArgs({});
|
|
59
|
+
setToolResult(null);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleSubmitTool = async (e: React.FormEvent) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
if (!selectedTool) return;
|
|
65
|
+
|
|
66
|
+
setExecutingTool(true);
|
|
67
|
+
setToolResult(null);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Pass JWT token and API key if available (check both jwtToken and OAuth token)
|
|
71
|
+
const result = await api.callTool(selectedTool.name, toolArgs, effectiveToken || undefined, apiKey || undefined);
|
|
72
|
+
setToolResult(result);
|
|
73
|
+
|
|
74
|
+
// Extract JWT token from ANY tool response (not just 'login')
|
|
75
|
+
// Check if result contains a token field at any level
|
|
76
|
+
if (result.content) {
|
|
77
|
+
try {
|
|
78
|
+
const content = result.content[0]?.text;
|
|
79
|
+
if (content) {
|
|
80
|
+
const parsed = JSON.parse(content);
|
|
81
|
+
// Check for token in multiple possible locations
|
|
82
|
+
const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
|
|
83
|
+
if (token) {
|
|
84
|
+
console.log('🔐 Token received from tool, saving to global state');
|
|
85
|
+
useStudioStore.getState().setJwtToken(token);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Ignore parsing errors
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Tool execution failed:', error);
|
|
94
|
+
setToolResult({ error: 'Tool execution failed' });
|
|
95
|
+
} finally {
|
|
96
|
+
setExecutingTool(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const filteredTools = tools.filter((tool) =>
|
|
101
|
+
tool.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
107
|
+
{/* Sticky Header */}
|
|
108
|
+
<div className="sticky top-0 z-10 border-b border-border/50 px-6 py-3 flex items-center justify-between bg-card/80 backdrop-blur-md shadow-sm">
|
|
109
|
+
<div className="flex items-center gap-3">
|
|
110
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-md">
|
|
111
|
+
<Wrench className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
112
|
+
</div>
|
|
113
|
+
<div>
|
|
114
|
+
<h1 className="text-lg font-bold text-foreground">Tools</h1>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<button onClick={loadTools} className="btn btn-primary text-sm px-4 py-2 gap-2">
|
|
118
|
+
<RefreshCw className="w-4 h-4" />
|
|
119
|
+
Refresh
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Content - ONLY this scrolls */}
|
|
124
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
125
|
+
<div className="max-w-7xl mx-auto px-6 py-6">
|
|
126
|
+
{/* Search */}
|
|
127
|
+
<div className="mb-6">
|
|
128
|
+
<input
|
|
129
|
+
type="text"
|
|
130
|
+
placeholder="Search tools..."
|
|
131
|
+
value={searchQuery}
|
|
132
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
133
|
+
className="input w-full"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Tools Grid */}
|
|
138
|
+
{loading.tools ? (
|
|
139
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
140
|
+
{[1, 2, 3].map((i) => (
|
|
141
|
+
<div key={i} className="card skeleton h-64"></div>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
) : filteredTools.length === 0 ? (
|
|
145
|
+
<div className="empty-state">
|
|
146
|
+
<AlertCircle className="empty-state-icon" />
|
|
147
|
+
<p className="empty-state-title">
|
|
148
|
+
{searchQuery ? 'No tools found matching your search' : 'No tools available'}
|
|
149
|
+
</p>
|
|
150
|
+
<p className="empty-state-description">
|
|
151
|
+
{searchQuery ? 'Try a different search term' : 'No MCP tools have been registered'}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
156
|
+
{filteredTools.map((tool) => (
|
|
157
|
+
<ToolCard key={tool.name} tool={tool} onExecute={handleExecuteTool} />
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Tool Executor Modal */}
|
|
166
|
+
{selectedTool && (
|
|
167
|
+
<div
|
|
168
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
|
169
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
170
|
+
onClick={() => setSelectedTool(null)}
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
className="w-[700px] max-h-[90vh] overflow-auto rounded-2xl border border-border p-6 bg-card shadow-2xl animate-scale-in"
|
|
174
|
+
onClick={(e) => e.stopPropagation()}
|
|
175
|
+
>
|
|
176
|
+
<div className="flex items-center justify-between mb-4">
|
|
177
|
+
<div className="flex items-center gap-3">
|
|
178
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
179
|
+
<Wrench className="w-5 h-5 text-primary" />
|
|
180
|
+
</div>
|
|
181
|
+
<h2 className="text-xl font-bold text-foreground">{selectedTool.name}</h2>
|
|
182
|
+
</div>
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => setSelectedTool(null)}
|
|
185
|
+
className="btn btn-ghost w-10 h-10 p-0"
|
|
186
|
+
>
|
|
187
|
+
<X className="w-5 h-5" />
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
192
|
+
{selectedTool.description || 'No description'}
|
|
193
|
+
</p>
|
|
194
|
+
|
|
195
|
+
<form onSubmit={handleSubmitTool}>
|
|
196
|
+
{/* Generate form inputs from schema */}
|
|
197
|
+
{selectedTool.inputSchema?.properties && typeof selectedTool.inputSchema.properties === 'object' && Object.keys(selectedTool.inputSchema.properties).length > 0 ? (
|
|
198
|
+
<>
|
|
199
|
+
{Object.entries(selectedTool.inputSchema.properties).map(([key, prop]: [string, any]) => {
|
|
200
|
+
const isRequired = selectedTool.inputSchema?.required?.includes(key);
|
|
201
|
+
|
|
202
|
+
// Handle different input types
|
|
203
|
+
if (prop.enum) {
|
|
204
|
+
// Enum/Select field
|
|
205
|
+
return (
|
|
206
|
+
<div key={key} className="mb-4">
|
|
207
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
208
|
+
{prop.title || key}
|
|
209
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
210
|
+
</label>
|
|
211
|
+
<select
|
|
212
|
+
className="input"
|
|
213
|
+
value={toolArgs[key] || prop.default || ''}
|
|
214
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })}
|
|
215
|
+
required={isRequired}
|
|
216
|
+
>
|
|
217
|
+
<option value="">Select...</option>
|
|
218
|
+
{prop.enum.map((val: any) => (
|
|
219
|
+
<option key={val} value={val}>
|
|
220
|
+
{val}
|
|
221
|
+
</option>
|
|
222
|
+
))}
|
|
223
|
+
</select>
|
|
224
|
+
{prop.description && (
|
|
225
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
} else if (prop.type === 'boolean') {
|
|
230
|
+
// Checkbox field
|
|
231
|
+
return (
|
|
232
|
+
<div key={key} className="mb-4">
|
|
233
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
234
|
+
<input
|
|
235
|
+
type="checkbox"
|
|
236
|
+
className="w-4 h-4"
|
|
237
|
+
checked={toolArgs[key] || false}
|
|
238
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })}
|
|
239
|
+
/>
|
|
240
|
+
<span className="text-sm font-medium text-foreground">
|
|
241
|
+
{prop.title || key}
|
|
242
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
243
|
+
</span>
|
|
244
|
+
</label>
|
|
245
|
+
{prop.description && (
|
|
246
|
+
<p className="text-xs text-muted-foreground mt-1 ml-6">{prop.description}</p>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
// Text/Number field
|
|
252
|
+
return (
|
|
253
|
+
<div key={key} className="mb-4">
|
|
254
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
255
|
+
{prop.title || key}
|
|
256
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
257
|
+
</label>
|
|
258
|
+
<input
|
|
259
|
+
type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'}
|
|
260
|
+
className="input"
|
|
261
|
+
value={toolArgs[key] || prop.default || ''}
|
|
262
|
+
onChange={(e) => {
|
|
263
|
+
const value = prop.type === 'number' || prop.type === 'integer'
|
|
264
|
+
? (e.target.value ? Number(e.target.value) : '')
|
|
265
|
+
: e.target.value;
|
|
266
|
+
setToolArgs({ ...toolArgs, [key]: value });
|
|
267
|
+
}}
|
|
268
|
+
required={isRequired}
|
|
269
|
+
placeholder={prop.description}
|
|
270
|
+
min={prop.minimum}
|
|
271
|
+
max={prop.maximum}
|
|
272
|
+
/>
|
|
273
|
+
{prop.description && (
|
|
274
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
})}
|
|
280
|
+
</>
|
|
281
|
+
) : (
|
|
282
|
+
<div className="mb-4 p-4 bg-muted/30 rounded-lg border border-border">
|
|
283
|
+
<p className="text-sm text-muted-foreground">No input required</p>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
<button
|
|
288
|
+
type="submit"
|
|
289
|
+
className="btn btn-primary w-full gap-2"
|
|
290
|
+
disabled={executingTool}
|
|
291
|
+
>
|
|
292
|
+
<Play className="w-4 h-4" />
|
|
293
|
+
{executingTool ? 'Executing...' : 'Execute Tool'}
|
|
294
|
+
</button>
|
|
295
|
+
</form>
|
|
296
|
+
|
|
297
|
+
{/* Result */}
|
|
298
|
+
{toolResult && (
|
|
299
|
+
<div className="mt-6 space-y-4">
|
|
300
|
+
<div>
|
|
301
|
+
<h3 className="font-semibold text-foreground mb-3">Result:</h3>
|
|
302
|
+
<pre className="bg-muted/30 p-4 rounded-lg text-sm overflow-auto max-h-64 text-foreground font-mono border border-border">
|
|
303
|
+
{JSON.stringify(toolResult, null, 2)}
|
|
304
|
+
</pre>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Widget UI Rendering */}
|
|
308
|
+
{(() => {
|
|
309
|
+
// Get widget URI from multiple sources (same as ToolCard)
|
|
310
|
+
const widgetUri =
|
|
311
|
+
selectedTool.widget?.route ||
|
|
312
|
+
selectedTool.outputTemplate ||
|
|
313
|
+
selectedTool._meta?.['ui/template'] ||
|
|
314
|
+
selectedTool._meta?.['openai/outputTemplate'];
|
|
315
|
+
|
|
316
|
+
return widgetUri && toolResult ? (
|
|
317
|
+
<div>
|
|
318
|
+
<h3 className="font-semibold text-foreground mb-3">UI Widget:</h3>
|
|
319
|
+
<div className="border border-border rounded-lg overflow-hidden h-64 bg-background shadow-inner">
|
|
320
|
+
<WidgetRenderer
|
|
321
|
+
uri={widgetUri}
|
|
322
|
+
data={(() => {
|
|
323
|
+
// Try to parse JSON from content[0].text, otherwise use raw result
|
|
324
|
+
if (toolResult.content?.[0]?.text) {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(toolResult.content[0].text);
|
|
327
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
328
|
+
if (parsed.success !== undefined && parsed.data !== undefined) {
|
|
329
|
+
return parsed.data;
|
|
330
|
+
}
|
|
331
|
+
return parsed;
|
|
332
|
+
} catch {
|
|
333
|
+
return { message: toolResult.content[0].text };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return toolResult;
|
|
337
|
+
})()}
|
|
338
|
+
className="w-full h-full"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
) : null;
|
|
343
|
+
})()}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
351
|
+
}
|