nitrostack 1.0.0 → 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/studio/README.md +140 -0
  6. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  7. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  8. package/src/studio/app/api/chat/route.ts +123 -0
  9. package/src/studio/app/api/health/checks/route.ts +42 -0
  10. package/src/studio/app/api/health/route.ts +13 -0
  11. package/src/studio/app/api/init/route.ts +85 -0
  12. package/src/studio/app/api/ping/route.ts +13 -0
  13. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  14. package/src/studio/app/api/prompts/route.ts +13 -0
  15. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  16. package/src/studio/app/api/resources/route.ts +13 -0
  17. package/src/studio/app/api/roots/route.ts +13 -0
  18. package/src/studio/app/api/sampling/route.ts +14 -0
  19. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  20. package/src/studio/app/api/tools/route.ts +23 -0
  21. package/src/studio/app/api/widget-examples/route.ts +44 -0
  22. package/src/studio/app/auth/callback/page.tsx +160 -0
  23. package/src/studio/app/auth/page.tsx +543 -0
  24. package/src/studio/app/chat/page.tsx +530 -0
  25. package/src/studio/app/chat/page.tsx.backup +390 -0
  26. package/src/studio/app/globals.css +410 -0
  27. package/src/studio/app/health/page.tsx +177 -0
  28. package/src/studio/app/layout.tsx +48 -0
  29. package/src/studio/app/page.tsx +337 -0
  30. package/src/studio/app/page.tsx.backup +346 -0
  31. package/src/studio/app/ping/page.tsx +204 -0
  32. package/src/studio/app/prompts/page.tsx +228 -0
  33. package/src/studio/app/resources/page.tsx +313 -0
  34. package/src/studio/components/EnlargeModal.tsx +116 -0
  35. package/src/studio/components/Sidebar.tsx +133 -0
  36. package/src/studio/components/ToolCard.tsx +108 -0
  37. package/src/studio/components/WidgetRenderer.tsx +99 -0
  38. package/src/studio/lib/api.ts +207 -0
  39. package/src/studio/lib/llm-service.ts +361 -0
  40. package/src/studio/lib/mcp-client.ts +168 -0
  41. package/src/studio/lib/store.ts +192 -0
  42. package/src/studio/lib/theme-provider.tsx +50 -0
  43. package/src/studio/lib/types.ts +107 -0
  44. package/src/studio/lib/widget-loader.ts +90 -0
  45. package/src/studio/middleware.ts +27 -0
  46. package/src/studio/next.config.js +16 -0
  47. package/src/studio/package-lock.json +2696 -0
  48. package/src/studio/package.json +34 -0
  49. package/src/studio/postcss.config.mjs +10 -0
  50. package/src/studio/tailwind.config.ts +67 -0
  51. package/src/studio/tsconfig.json +41 -0
  52. package/templates/typescript-auth/.env.example +23 -0
  53. package/templates/typescript-auth/src/app.module.ts +103 -0
  54. package/templates/typescript-auth/src/db/database.ts +163 -0
  55. package/templates/typescript-auth/src/db/seed.ts +374 -0
  56. package/templates/typescript-auth/src/db/setup.ts +87 -0
  57. package/templates/typescript-auth/src/events/analytics.service.ts +52 -0
  58. package/templates/typescript-auth/src/events/notification.service.ts +40 -0
  59. package/templates/typescript-auth/src/filters/global-exception.filter.ts +28 -0
  60. package/templates/typescript-auth/src/guards/README.md +75 -0
  61. package/templates/typescript-auth/src/guards/jwt.guard.ts +105 -0
  62. package/templates/typescript-auth/src/health/database.health.ts +41 -0
  63. package/templates/typescript-auth/src/index.ts +26 -0
  64. package/templates/typescript-auth/src/interceptors/transform.interceptor.ts +24 -0
  65. package/templates/typescript-auth/src/middleware/logging.middleware.ts +42 -0
  66. package/templates/typescript-auth/src/modules/addresses/addresses.module.ts +16 -0
  67. package/templates/typescript-auth/src/modules/addresses/addresses.prompts.ts +114 -0
  68. package/templates/typescript-auth/src/modules/addresses/addresses.resources.ts +40 -0
  69. package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +241 -0
  70. package/templates/typescript-auth/src/modules/auth/auth.module.ts +16 -0
  71. package/templates/typescript-auth/src/modules/auth/auth.prompts.ts +147 -0
  72. package/templates/typescript-auth/src/modules/auth/auth.resources.ts +84 -0
  73. package/templates/typescript-auth/src/modules/auth/auth.tools.ts +139 -0
  74. package/templates/typescript-auth/src/modules/cart/cart.module.ts +16 -0
  75. package/templates/typescript-auth/src/modules/cart/cart.prompts.ts +95 -0
  76. package/templates/typescript-auth/src/modules/cart/cart.resources.ts +44 -0
  77. package/templates/typescript-auth/src/modules/cart/cart.tools.ts +281 -0
  78. package/templates/typescript-auth/src/modules/orders/orders.module.ts +16 -0
  79. package/templates/typescript-auth/src/modules/orders/orders.prompts.ts +88 -0
  80. package/templates/typescript-auth/src/modules/orders/orders.resources.ts +48 -0
  81. package/templates/typescript-auth/src/modules/orders/orders.tools.ts +281 -0
  82. package/templates/typescript-auth/src/modules/products/products.module.ts +16 -0
  83. package/templates/typescript-auth/src/modules/products/products.prompts.ts +146 -0
  84. package/templates/typescript-auth/src/modules/products/products.resources.ts +98 -0
  85. package/templates/typescript-auth/src/modules/products/products.tools.ts +266 -0
  86. package/templates/typescript-auth/src/pipes/validation.pipe.ts +42 -0
  87. package/templates/typescript-auth/src/services/database.service.ts +90 -0
  88. package/templates/typescript-auth/src/widgets/app/add-to-cart/page.tsx +122 -0
  89. package/templates/typescript-auth/src/widgets/app/address-added/page.tsx +116 -0
  90. package/templates/typescript-auth/src/widgets/app/address-deleted/page.tsx +105 -0
  91. package/templates/typescript-auth/src/widgets/app/address-list/page.tsx +139 -0
  92. package/templates/typescript-auth/src/widgets/app/address-updated/page.tsx +153 -0
  93. package/templates/typescript-auth/src/widgets/app/cart-cleared/page.tsx +86 -0
  94. package/templates/typescript-auth/src/widgets/app/cart-updated/page.tsx +116 -0
  95. package/templates/typescript-auth/src/widgets/app/categories/page.tsx +134 -0
  96. package/templates/typescript-auth/src/widgets/app/layout.tsx +21 -0
  97. package/templates/typescript-auth/src/widgets/app/login-result/page.tsx +129 -0
  98. package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +206 -0
  99. package/templates/typescript-auth/src/widgets/app/order-details/page.tsx +225 -0
  100. package/templates/typescript-auth/src/widgets/app/order-history/page.tsx +218 -0
  101. package/templates/typescript-auth/src/widgets/app/product-card/page.tsx +121 -0
  102. package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +173 -0
  103. package/templates/typescript-auth/src/widgets/app/shopping-cart/page.tsx +187 -0
  104. package/templates/typescript-auth/src/widgets/app/whoami/page.tsx +165 -0
  105. package/templates/typescript-auth/src/widgets/next.config.js +38 -0
  106. package/templates/typescript-auth/src/widgets/package.json +18 -0
  107. package/templates/typescript-auth/src/widgets/styles/ecommerce.ts +169 -0
  108. package/templates/typescript-auth/src/widgets/tsconfig.json +28 -0
  109. package/templates/typescript-auth/src/widgets/types/tool-data.ts +141 -0
  110. package/templates/typescript-auth/src/widgets/widget-manifest.json +464 -0
  111. package/templates/typescript-auth/tsconfig.json +27 -0
  112. package/templates/typescript-auth-api-key/.env +15 -0
  113. package/templates/typescript-auth-api-key/.env.example +4 -0
  114. package/templates/typescript-auth-api-key/src/app.module.ts +38 -0
  115. package/templates/typescript-auth-api-key/src/guards/apikey.guard.ts +47 -0
  116. package/templates/typescript-auth-api-key/src/guards/multi-auth.guard.ts +157 -0
  117. package/templates/typescript-auth-api-key/src/health/system.health.ts +55 -0
  118. package/templates/typescript-auth-api-key/src/index.ts +47 -0
  119. package/templates/typescript-auth-api-key/src/modules/calculator/calculator.module.ts +12 -0
  120. package/templates/typescript-auth-api-key/src/modules/calculator/calculator.prompts.ts +73 -0
  121. package/templates/typescript-auth-api-key/src/modules/calculator/calculator.resources.ts +60 -0
  122. package/templates/typescript-auth-api-key/src/modules/calculator/calculator.tools.ts +71 -0
  123. package/templates/typescript-auth-api-key/src/modules/demo/demo.module.ts +18 -0
  124. package/templates/typescript-auth-api-key/src/modules/demo/demo.tools.ts +155 -0
  125. package/templates/typescript-auth-api-key/src/modules/demo/multi-auth.tools.ts +123 -0
  126. package/templates/typescript-auth-api-key/src/widgets/app/calculator-operations/page.tsx +133 -0
  127. package/templates/typescript-auth-api-key/src/widgets/app/calculator-result/page.tsx +134 -0
  128. package/templates/typescript-auth-api-key/src/widgets/app/layout.tsx +14 -0
  129. package/templates/typescript-auth-api-key/src/widgets/next.config.js +37 -0
  130. package/templates/typescript-auth-api-key/src/widgets/package.json +24 -0
  131. package/templates/typescript-auth-api-key/src/widgets/tsconfig.json +28 -0
  132. package/templates/typescript-auth-api-key/src/widgets/widget-manifest.json +48 -0
  133. package/templates/typescript-auth-api-key/tsconfig.json +23 -0
  134. package/templates/typescript-oauth/.env.example +91 -0
  135. package/templates/typescript-oauth/src/app.module.ts +89 -0
  136. package/templates/typescript-oauth/src/guards/oauth.guard.ts +127 -0
  137. package/templates/typescript-oauth/src/index.ts +74 -0
  138. package/templates/typescript-oauth/src/modules/demo/demo.module.ts +16 -0
  139. package/templates/typescript-oauth/src/modules/demo/demo.tools.ts +190 -0
  140. package/templates/typescript-oauth/src/widgets/app/calculator-operations/page.tsx +133 -0
  141. package/templates/typescript-oauth/src/widgets/app/calculator-result/page.tsx +134 -0
  142. package/templates/typescript-oauth/src/widgets/app/layout.tsx +14 -0
  143. package/templates/typescript-oauth/src/widgets/next.config.js +37 -0
  144. package/templates/typescript-oauth/src/widgets/package.json +24 -0
  145. package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
  146. package/templates/typescript-oauth/src/widgets/widget-manifest.json +48 -0
  147. package/templates/typescript-oauth/tsconfig.json +23 -0
  148. package/templates/typescript-starter/.env.example +4 -0
  149. package/templates/typescript-starter/src/app.module.ts +34 -0
  150. package/templates/typescript-starter/src/health/system.health.ts +55 -0
  151. package/templates/typescript-starter/src/index.ts +27 -0
  152. package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
  153. package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
  154. package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +60 -0
  155. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +71 -0
  156. package/templates/typescript-starter/src/widgets/app/calculator-operations/page.tsx +133 -0
  157. package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +134 -0
  158. package/templates/typescript-starter/src/widgets/app/layout.tsx +14 -0
  159. package/templates/typescript-starter/src/widgets/next.config.js +37 -0
  160. package/templates/typescript-starter/src/widgets/package.json +24 -0
  161. package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
  162. package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
  163. package/templates/typescript-starter/tsconfig.json +23 -0
  164. package/LICENSE_URLS_UPDATE_COMPLETE.md +0 -388
@@ -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
+