nitrostack 1.0.15 → 1.0.17
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/README.md +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +2 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/mcp-dev-wrapper.js +30 -17
- package/dist/cli/mcp-dev-wrapper.js.map +1 -1
- package/dist/core/app-decorator.js +2 -2
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.js +2 -2
- package/dist/core/builders.js.map +1 -1
- package/dist/core/resource.js +1 -1
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.js +2 -2
- package/dist/core/server.js.map +1 -1
- package/dist/core/transports/http-server.d.ts.map +1 -1
- package/dist/core/transports/http-server.js +21 -1
- package/dist/core/transports/http-server.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +155 -28
- package/src/studio/app/api/init/route.ts +28 -4
- package/src/studio/app/auth/page.tsx +13 -9
- package/src/studio/app/chat/page.tsx +599 -133
- package/src/studio/app/health/page.tsx +101 -99
- package/src/studio/app/layout.tsx +24 -4
- package/src/studio/app/page.tsx +61 -56
- package/src/studio/app/ping/page.tsx +13 -8
- package/src/studio/app/prompts/page.tsx +72 -70
- package/src/studio/app/resources/page.tsx +88 -86
- package/src/studio/app/settings/page.tsx +270 -0
- package/src/studio/components/EnlargeModal.tsx +21 -15
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +197 -35
- package/src/studio/components/ToolCard.tsx +27 -9
- package/src/studio/components/WidgetRenderer.tsx +4 -2
- package/src/studio/lib/http-client-transport.ts +222 -0
- package/src/studio/lib/llm-service.ts +119 -0
- package/src/studio/lib/log-manager.ts +76 -0
- package/src/studio/lib/mcp-client.ts +103 -13
- package/src/studio/package-lock.json +3129 -0
- package/src/studio/package.json +1 -0
- package/templates/typescript-auth/README.md +3 -1
- package/templates/typescript-auth/src/db/database.ts +5 -8
- package/templates/typescript-auth/src/index.ts +13 -2
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +49 -6
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +13 -17
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +38 -16
- package/templates/typescript-auth/src/modules/products/products.tools.ts +4 -4
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +25 -0
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +26 -1
- package/templates/typescript-auth-api-key/README.md +3 -1
- package/templates/typescript-auth-api-key/src/index.ts +11 -3
- package/templates/typescript-starter/README.md +3 -1
|
@@ -12,17 +12,22 @@ import {
|
|
|
12
12
|
Activity,
|
|
13
13
|
Shield,
|
|
14
14
|
Wifi,
|
|
15
|
-
Zap
|
|
15
|
+
Zap,
|
|
16
|
+
Settings,
|
|
17
|
+
Sparkles,
|
|
18
|
+
Terminal
|
|
16
19
|
} from 'lucide-react';
|
|
17
20
|
|
|
18
|
-
const navItems: Array<{ id: TabType; label: string; icon: any; path: string }> = [
|
|
21
|
+
const navItems: Array<{ id: TabType | 'settings' | 'logs'; label: string; icon: any; path: string }> = [
|
|
19
22
|
{ id: 'tools', label: 'Tools', icon: Wrench, path: '/' },
|
|
20
23
|
{ id: 'chat', label: 'AI Chat', icon: MessageSquare, path: '/chat' },
|
|
21
24
|
{ id: 'resources', label: 'Resources', icon: Package, path: '/resources' },
|
|
22
25
|
{ id: 'prompts', label: 'Prompts', icon: FileText, path: '/prompts' },
|
|
23
26
|
{ id: 'health', label: 'Health', icon: Activity, path: '/health' },
|
|
27
|
+
{ id: 'logs', label: 'Logs', icon: Terminal, path: '/logs' },
|
|
24
28
|
{ id: 'auth', label: 'OAuth 2.1', icon: Shield, path: '/auth' },
|
|
25
29
|
{ id: 'ping', label: 'Ping', icon: Wifi, path: '/ping' },
|
|
30
|
+
{ id: 'settings', label: 'Settings', icon: Settings, path: '/settings' },
|
|
26
31
|
];
|
|
27
32
|
|
|
28
33
|
export function Sidebar() {
|
|
@@ -30,11 +35,15 @@ export function Sidebar() {
|
|
|
30
35
|
const router = useRouter();
|
|
31
36
|
const pathname = usePathname();
|
|
32
37
|
const [mounted, setMounted] = useState(false);
|
|
38
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
33
39
|
|
|
34
40
|
useEffect(() => {
|
|
35
41
|
setMounted(true);
|
|
36
42
|
// Force dark mode
|
|
37
43
|
document.documentElement.className = 'dark antialiased';
|
|
44
|
+
// Load collapse state from localStorage
|
|
45
|
+
const saved = localStorage.getItem('sidebar_collapsed');
|
|
46
|
+
if (saved !== null) setIsCollapsed(saved === 'true');
|
|
38
47
|
}, []);
|
|
39
48
|
|
|
40
49
|
const handleNavigation = (path: string) => {
|
|
@@ -47,53 +56,157 @@ export function Sidebar() {
|
|
|
47
56
|
return 'disconnected';
|
|
48
57
|
};
|
|
49
58
|
|
|
59
|
+
const toggleSidebar = () => {
|
|
60
|
+
const newState = !isCollapsed;
|
|
61
|
+
setIsCollapsed(newState);
|
|
62
|
+
localStorage.setItem('sidebar_collapsed', String(newState));
|
|
63
|
+
// Dispatch custom event to update layout
|
|
64
|
+
window.dispatchEvent(new Event('sidebar-toggle'));
|
|
65
|
+
};
|
|
66
|
+
|
|
50
67
|
if (!mounted) return null;
|
|
51
68
|
|
|
52
69
|
return (
|
|
53
|
-
<nav className=
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
<nav className={`fixed left-0 top-0 h-screen glass flex flex-col z-50 border-r border-border/50 transition-all duration-300 ease-in-out ${
|
|
71
|
+
isCollapsed ? 'w-16' : 'w-60 md:w-60'
|
|
72
|
+
} ${isCollapsed ? '' : 'max-md:w-16'}`}>
|
|
73
|
+
{/* Compact Professional Header */}
|
|
74
|
+
<div className="relative p-3 border-b border-border/50 bg-gradient-to-b from-card/80 to-transparent">
|
|
75
|
+
<div className="flex items-center justify-between mb-2">
|
|
76
|
+
{/* Minimalist Professional Logo */}
|
|
77
|
+
<div
|
|
78
|
+
className="flex items-center gap-2 group cursor-pointer flex-1"
|
|
79
|
+
onClick={() => handleNavigation('/')}
|
|
80
|
+
>
|
|
81
|
+
<div className="relative flex-shrink-0">
|
|
82
|
+
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-primary to-amber-500 blur opacity-30 group-hover:opacity-50 transition-opacity duration-300" />
|
|
83
|
+
<div className="relative w-9 h-9 rounded-lg bg-gradient-to-br from-slate-900 to-slate-800 border border-primary/30 flex items-center justify-center shadow-lg group-hover:shadow-primary/30 transition-all duration-300 overflow-hidden">
|
|
84
|
+
{/* Geometric N logo */}
|
|
85
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="relative z-10">
|
|
86
|
+
<path d="M4 16V4L16 16V4" stroke="url(#gradient)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
87
|
+
<defs>
|
|
88
|
+
<linearGradient id="gradient" x1="4" y1="4" x2="16" y2="16">
|
|
89
|
+
<stop offset="0%" stopColor="#F59E0B" />
|
|
90
|
+
<stop offset="100%" stopColor="#F97316" />
|
|
91
|
+
</linearGradient>
|
|
92
|
+
</defs>
|
|
93
|
+
</svg>
|
|
94
|
+
{/* Subtle glow effect */}
|
|
95
|
+
<div className="absolute inset-0 bg-gradient-to-tr from-primary/20 to-amber-500/20 group-hover:opacity-100 opacity-0 transition-opacity duration-300" />
|
|
96
|
+
</div>
|
|
59
97
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
|
|
99
|
+
{!isCollapsed && (
|
|
100
|
+
<div className="flex-1 overflow-hidden">
|
|
101
|
+
<h1 className="text-base font-bold bg-gradient-to-r from-primary to-amber-500 bg-clip-text text-transparent tracking-tight whitespace-nowrap">
|
|
102
|
+
NitroStudio
|
|
63
103
|
</h1>
|
|
64
|
-
|
|
104
|
+
<p className="text-[9px] text-muted-foreground font-medium uppercase tracking-wider">
|
|
105
|
+
MCP Suite
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
65
109
|
</div>
|
|
110
|
+
|
|
111
|
+
{/* Toggle Button */}
|
|
112
|
+
<button
|
|
113
|
+
onClick={toggleSidebar}
|
|
114
|
+
className="flex-shrink-0 w-7 h-7 rounded-md bg-muted/50 hover:bg-muted border border-border/50 hover:border-primary/30 flex items-center justify-center transition-all duration-300 group/toggle"
|
|
115
|
+
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
116
|
+
>
|
|
117
|
+
<svg
|
|
118
|
+
width="14"
|
|
119
|
+
height="14"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
className={`text-muted-foreground group-hover/toggle:text-primary transition-all duration-300 ${
|
|
123
|
+
isCollapsed ? 'rotate-180' : ''
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
127
|
+
</svg>
|
|
128
|
+
</button>
|
|
66
129
|
</div>
|
|
67
130
|
|
|
131
|
+
{/* Compact Status Indicators */}
|
|
132
|
+
{!isCollapsed ? (
|
|
133
|
+
<div className="space-y-1.5">
|
|
68
134
|
{/* Connection Status */}
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
<div className="relative group">
|
|
136
|
+
<div className="relative flex items-center gap-2 px-2 py-1.5 rounded-md bg-card/60 border border-border/50 backdrop-blur-sm group-hover:border-primary/30 transition-all duration-300">
|
|
137
|
+
<div className="relative flex items-center justify-center flex-shrink-0">
|
|
138
|
+
{getConnectionStatus() === 'connected' && (
|
|
139
|
+
<div className="absolute inset-0 rounded-full bg-emerald-500/30 animate-ping" />
|
|
140
|
+
)}
|
|
71
141
|
<div
|
|
72
|
-
|
|
142
|
+
className={`relative w-2 h-2 rounded-full transition-all duration-300 ${
|
|
73
143
|
getConnectionStatus() === 'connected'
|
|
74
144
|
? 'bg-emerald-500'
|
|
75
145
|
: getConnectionStatus() === 'connecting'
|
|
76
|
-
|
|
146
|
+
? 'bg-amber-500 animate-pulse'
|
|
77
147
|
: 'bg-rose-500'
|
|
78
148
|
}`}
|
|
79
149
|
style={{
|
|
80
150
|
boxShadow: getConnectionStatus() === 'connected'
|
|
81
|
-
|
|
151
|
+
? '0 0 8px rgba(34, 197, 94, 0.6)'
|
|
82
152
|
: getConnectionStatus() === 'connecting'
|
|
83
|
-
|
|
84
|
-
|
|
153
|
+
? '0 0 8px rgba(245, 158, 11, 0.6)'
|
|
154
|
+
: '0 0 8px rgba(239, 68, 68, 0.6)'
|
|
85
155
|
}}
|
|
86
156
|
/>
|
|
87
157
|
</div>
|
|
88
|
-
|
|
158
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-foreground">
|
|
89
159
|
{connection.status}
|
|
90
160
|
</span>
|
|
91
161
|
</div>
|
|
92
162
|
</div>
|
|
93
163
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
164
|
+
{/* Transport Mode */}
|
|
165
|
+
<div className="relative group">
|
|
166
|
+
<div className="relative px-2 py-1.5 rounded-md bg-primary/5 border border-primary/20 backdrop-blur-sm group-hover:border-primary/30 transition-all duration-300">
|
|
167
|
+
<div className="flex items-center justify-between text-[10px]">
|
|
168
|
+
<span className="font-medium text-muted-foreground">Transport:</span>
|
|
169
|
+
<span className="font-bold text-primary uppercase">
|
|
170
|
+
{typeof localStorage !== 'undefined' ? localStorage.getItem('mcp_transport') || 'STDIO' : 'STDIO'}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="flex flex-col items-center gap-1.5">
|
|
178
|
+
{/* Collapsed: Just the status dot */}
|
|
179
|
+
<div
|
|
180
|
+
className="relative w-7 h-7 rounded-md bg-card/60 border border-border/50 flex items-center justify-center group hover:border-primary/30 transition-all"
|
|
181
|
+
title={`Status: ${connection.status}`}
|
|
182
|
+
>
|
|
183
|
+
{getConnectionStatus() === 'connected' && (
|
|
184
|
+
<div className="absolute inset-0 rounded-md bg-emerald-500/20 animate-ping" />
|
|
185
|
+
)}
|
|
186
|
+
<div
|
|
187
|
+
className={`relative w-2 h-2 rounded-full ${
|
|
188
|
+
getConnectionStatus() === 'connected'
|
|
189
|
+
? 'bg-emerald-500'
|
|
190
|
+
: getConnectionStatus() === 'connecting'
|
|
191
|
+
? 'bg-amber-500 animate-pulse'
|
|
192
|
+
: 'bg-rose-500'
|
|
193
|
+
}`}
|
|
194
|
+
style={{
|
|
195
|
+
boxShadow: getConnectionStatus() === 'connected'
|
|
196
|
+
? '0 0 8px rgba(34, 197, 94, 0.8)'
|
|
197
|
+
: getConnectionStatus() === 'connecting'
|
|
198
|
+
? '0 0 8px rgba(245, 158, 11, 0.8)'
|
|
199
|
+
: '0 0 8px rgba(239, 68, 68, 0.8)'
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Compact Navigation */}
|
|
208
|
+
<div className="flex-1 overflow-y-auto py-2 px-2 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
|
209
|
+
<div className={`space-y-0.5 ${isCollapsed ? 'flex flex-col items-center' : ''}`}>
|
|
97
210
|
{navItems.map((item) => {
|
|
98
211
|
const Icon = item.icon;
|
|
99
212
|
const isActive = pathname === item.path;
|
|
@@ -102,31 +215,80 @@ export function Sidebar() {
|
|
|
102
215
|
<button
|
|
103
216
|
key={item.id}
|
|
104
217
|
onClick={() => handleNavigation(item.path)}
|
|
218
|
+
title={isCollapsed ? item.label : undefined}
|
|
105
219
|
className={`
|
|
106
|
-
|
|
220
|
+
relative flex items-center gap-2 text-sm font-medium rounded-lg transition-all duration-300 group overflow-hidden
|
|
221
|
+
${isCollapsed ? 'w-10 h-10 justify-center' : 'w-full px-3 py-2'}
|
|
107
222
|
${isActive
|
|
108
|
-
? 'bg-primary/10 text-primary shadow-
|
|
109
|
-
: 'text-foreground hover:bg-
|
|
223
|
+
? 'bg-gradient-to-r from-primary/15 to-amber-500/10 text-primary shadow-md ring-1 ring-primary/30'
|
|
224
|
+
: 'text-foreground/70 hover:bg-primary/5 hover:text-primary'
|
|
110
225
|
}
|
|
111
226
|
`}
|
|
112
227
|
>
|
|
113
|
-
|
|
114
|
-
|
|
228
|
+
{/* Active indicator */}
|
|
229
|
+
{isActive && !isCollapsed && (
|
|
230
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-gradient-to-b from-primary to-amber-500 rounded-r-full" />
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* Icon */}
|
|
234
|
+
<div className={`relative flex-shrink-0 ${isActive ? 'scale-105' : 'group-hover:scale-105'} transition-transform duration-300`}>
|
|
235
|
+
<Icon
|
|
236
|
+
className={`w-4 h-4 ${
|
|
237
|
+
isActive
|
|
238
|
+
? 'text-primary'
|
|
239
|
+
: 'text-muted-foreground group-hover:text-primary'
|
|
240
|
+
}`}
|
|
241
|
+
strokeWidth={isActive ? 2.5 : 2}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Label - only show when expanded */}
|
|
246
|
+
{!isCollapsed && (
|
|
247
|
+
<span className="text-xs whitespace-nowrap overflow-hidden">
|
|
248
|
+
{item.label}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{/* Subtle shine effect */}
|
|
253
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-out pointer-events-none" />
|
|
115
254
|
</button>
|
|
116
255
|
);
|
|
117
256
|
})}
|
|
118
257
|
</div>
|
|
119
258
|
</div>
|
|
120
259
|
|
|
121
|
-
{/* Footer */}
|
|
122
|
-
<div className="p-
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
260
|
+
{/* Compact Footer */}
|
|
261
|
+
<div className="p-2 border-t border-border/50 bg-gradient-to-t from-card/60 to-transparent backdrop-blur-sm">
|
|
262
|
+
{!isCollapsed ? (
|
|
263
|
+
<div className="space-y-1.5">
|
|
264
|
+
{/* Version info */}
|
|
265
|
+
<div className="px-2 py-1.5 rounded-md bg-muted/30 border border-border/30">
|
|
266
|
+
<div className="flex items-center justify-between text-[9px]">
|
|
267
|
+
<span className="font-medium text-muted-foreground uppercase tracking-wide">MCP v1.0</span>
|
|
268
|
+
<span className="font-bold text-foreground">NitroStack</span>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Copyright */}
|
|
273
|
+
<div className="text-center">
|
|
274
|
+
<p className="text-[8px] text-muted-foreground/50 font-medium">
|
|
275
|
+
© 2025 NitroCloud
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
) : (
|
|
280
|
+
<div className="flex flex-col items-center">
|
|
281
|
+
{/* Collapsed: Minimal version indicator */}
|
|
282
|
+
<div
|
|
283
|
+
className="w-10 h-10 rounded-md bg-muted/30 border border-border/30 flex items-center justify-center group hover:border-primary/30 transition-all"
|
|
284
|
+
title="MCP v1.0 • NitroStack"
|
|
285
|
+
>
|
|
286
|
+
<span className="text-[9px] font-bold text-primary group-hover:scale-110 transition-transform">
|
|
287
|
+
v1
|
|
288
|
+
</span>
|
|
128
289
|
</div>
|
|
129
290
|
</div>
|
|
291
|
+
)}
|
|
130
292
|
</div>
|
|
131
293
|
</nav>
|
|
132
294
|
);
|
|
@@ -4,7 +4,7 @@ import type { Tool } from '@/lib/types';
|
|
|
4
4
|
import { useStudioStore } from '@/lib/store';
|
|
5
5
|
import { WidgetRenderer } from './WidgetRenderer';
|
|
6
6
|
import { useRouter } from 'next/navigation';
|
|
7
|
-
import { Zap, Palette, Maximize2, Play, Sparkles } from 'lucide-react';
|
|
7
|
+
import { Zap, Palette, Maximize2, Play, Sparkles, MessageSquare } from 'lucide-react';
|
|
8
8
|
|
|
9
9
|
interface ToolCardProps {
|
|
10
10
|
tool: Tool;
|
|
@@ -28,7 +28,17 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
|
|
|
28
28
|
|
|
29
29
|
const handleUseInChat = (e: React.MouseEvent) => {
|
|
30
30
|
e.stopPropagation();
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
// Build the tool execution message
|
|
33
|
+
const toolMessage = `Use the ${tool.name} tool`;
|
|
34
|
+
|
|
35
|
+
// Store the message in localStorage
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
window.localStorage.setItem('chatInput', toolMessage);
|
|
38
|
+
window.localStorage.setItem('suggestedTool', tool.name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
router.push('/chat');
|
|
32
42
|
};
|
|
33
43
|
|
|
34
44
|
const handleEnlarge = (e: React.MouseEvent) => {
|
|
@@ -85,22 +95,30 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
|
|
|
85
95
|
)}
|
|
86
96
|
|
|
87
97
|
{/* Action Buttons */}
|
|
88
|
-
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
98
|
+
<div className="flex flex-wrap items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
89
99
|
{hasWidget && exampleData && (
|
|
90
100
|
<button
|
|
91
101
|
onClick={handleEnlarge}
|
|
92
|
-
className="btn btn-secondary flex-1 text-sm gap-2"
|
|
102
|
+
className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
93
103
|
>
|
|
94
|
-
<Maximize2 className="w-4 h-4" />
|
|
95
|
-
<span>Enlarge</span>
|
|
104
|
+
<Maximize2 className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
105
|
+
<span className="truncate">Enlarge</span>
|
|
96
106
|
</button>
|
|
97
107
|
)}
|
|
98
108
|
<button
|
|
99
109
|
onClick={() => onExecute(tool)}
|
|
100
|
-
className="btn btn-primary flex-1 text-sm gap-2"
|
|
110
|
+
className="btn btn-primary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
101
111
|
>
|
|
102
|
-
<Play className="w-4 h-4" />
|
|
103
|
-
<span>Execute</span>
|
|
112
|
+
<Play className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
113
|
+
<span className="truncate">Execute</span>
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
onClick={handleUseInChat}
|
|
117
|
+
className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
118
|
+
title="Use in Chat"
|
|
119
|
+
>
|
|
120
|
+
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
121
|
+
<span className="truncate">Chat</span>
|
|
104
122
|
</button>
|
|
105
123
|
</div>
|
|
106
124
|
</div>
|
|
@@ -82,14 +82,16 @@ export function WidgetRenderer({ uri, data, className = '' }: WidgetRendererProp
|
|
|
82
82
|
}
|
|
83
83
|
}, [uri, data, isDevMode]);
|
|
84
84
|
|
|
85
|
+
const isInChat = className?.includes('widget-in-chat');
|
|
86
|
+
|
|
85
87
|
return (
|
|
86
88
|
<iframe
|
|
87
89
|
ref={iframeRef}
|
|
88
90
|
className={className}
|
|
89
91
|
sandbox="allow-scripts allow-same-origin"
|
|
90
92
|
style={{
|
|
91
|
-
width: '100%',
|
|
92
|
-
height: '100%',
|
|
93
|
+
width: isInChat ? '650px' : '100%',
|
|
94
|
+
height: isInChat ? '450px' : '100%',
|
|
93
95
|
border: 'none',
|
|
94
96
|
background: 'transparent',
|
|
95
97
|
}}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client Transport for MCP
|
|
3
|
+
*
|
|
4
|
+
* Implements HTTP-based communication with MCP servers using SSE for server-to-client messages
|
|
5
|
+
* and POST for client-to-server messages.
|
|
6
|
+
*
|
|
7
|
+
* Note: This uses EventSource which is browser-only. For server-side usage, you need eventsource polyfill.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
11
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
|
|
13
|
+
// EventSource type that works in both browser and Node.js
|
|
14
|
+
type EventSourceType = typeof EventSource extends { prototype: infer T } ? T : any;
|
|
15
|
+
|
|
16
|
+
export interface HttpClientTransportOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Base URL of the MCP server (e.g., http://localhost:3000)
|
|
19
|
+
*/
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base path for MCP endpoints (default: '/mcp')
|
|
24
|
+
*/
|
|
25
|
+
basePath?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional headers to include in requests (e.g., Authorization)
|
|
29
|
+
*/
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* HTTP Client Transport
|
|
35
|
+
*
|
|
36
|
+
* Connects to an MCP server over HTTP using:
|
|
37
|
+
* - SSE (Server-Sent Events) for receiving messages from server
|
|
38
|
+
* - HTTP POST for sending messages to server
|
|
39
|
+
*/
|
|
40
|
+
export class HttpClientTransport implements Transport {
|
|
41
|
+
private baseUrl: string;
|
|
42
|
+
private basePath: string;
|
|
43
|
+
private headers: Record<string, string>;
|
|
44
|
+
private eventSource: EventSourceType | null = null;
|
|
45
|
+
private clientId: string;
|
|
46
|
+
private messageHandler?: (message: JSONRPCMessage) => Promise<void>;
|
|
47
|
+
private closeHandler?: () => void;
|
|
48
|
+
private errorHandler?: (error: Error) => void;
|
|
49
|
+
private isConnected = false;
|
|
50
|
+
private EventSourceImpl: any;
|
|
51
|
+
|
|
52
|
+
constructor(options: HttpClientTransportOptions) {
|
|
53
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
54
|
+
this.basePath = options.basePath || '/mcp';
|
|
55
|
+
this.headers = options.headers || {};
|
|
56
|
+
this.clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
57
|
+
|
|
58
|
+
// Use native EventSource in browser, require polyfill in Node.js
|
|
59
|
+
if (typeof EventSource !== 'undefined') {
|
|
60
|
+
this.EventSourceImpl = EventSource;
|
|
61
|
+
} else {
|
|
62
|
+
// In Node.js environment, try to load eventsource package
|
|
63
|
+
try {
|
|
64
|
+
this.EventSourceImpl = require('eventsource');
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'EventSource is not available. In Node.js, install "eventsource" package: npm install eventsource'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Start the transport by connecting to SSE endpoint
|
|
75
|
+
*/
|
|
76
|
+
async start(): Promise<void> {
|
|
77
|
+
if (this.isConnected) {
|
|
78
|
+
console.warn('⚠️ HTTP transport already connected');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
try {
|
|
84
|
+
const sseUrl = `${this.baseUrl}${this.basePath}/sse?clientId=${this.clientId}`;
|
|
85
|
+
console.log('🔌 Connecting to SSE endpoint:', sseUrl);
|
|
86
|
+
|
|
87
|
+
// Create EventSource for receiving server messages
|
|
88
|
+
this.eventSource = new this.EventSourceImpl(sseUrl) as EventSourceType;
|
|
89
|
+
|
|
90
|
+
this.eventSource.onopen = () => {
|
|
91
|
+
console.log('✅ SSE connection established');
|
|
92
|
+
this.isConnected = true;
|
|
93
|
+
resolve();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.eventSource.onmessage = (event) => {
|
|
97
|
+
try {
|
|
98
|
+
const message = JSON.parse(event.data) as JSONRPCMessage;
|
|
99
|
+
console.log('📨 Received message from server:', message);
|
|
100
|
+
|
|
101
|
+
if (this.messageHandler) {
|
|
102
|
+
this.messageHandler(message).catch((error) => {
|
|
103
|
+
console.error('Error handling message:', error);
|
|
104
|
+
if (this.errorHandler) {
|
|
105
|
+
this.errorHandler(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Failed to parse SSE message:', error);
|
|
111
|
+
if (this.errorHandler) {
|
|
112
|
+
this.errorHandler(error as Error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this.eventSource.onerror = (error) => {
|
|
118
|
+
console.error('❌ SSE connection error:', error);
|
|
119
|
+
this.isConnected = false;
|
|
120
|
+
|
|
121
|
+
if (this.errorHandler) {
|
|
122
|
+
this.errorHandler(new Error('SSE connection failed'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!this.isConnected) {
|
|
126
|
+
// Connection failed during initial setup
|
|
127
|
+
reject(new Error('Failed to establish SSE connection'));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Set a timeout for connection establishment
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
if (!this.isConnected) {
|
|
134
|
+
this.eventSource?.close();
|
|
135
|
+
reject(new Error('SSE connection timeout'));
|
|
136
|
+
}
|
|
137
|
+
}, 10000); // 10 second timeout
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
reject(error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Send a message to the server
|
|
147
|
+
*/
|
|
148
|
+
async send(message: JSONRPCMessage): Promise<void> {
|
|
149
|
+
if (!this.isConnected) {
|
|
150
|
+
throw new Error('HTTP transport not connected');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const url = `${this.baseUrl}${this.basePath}/message`;
|
|
155
|
+
console.log('📤 Sending message to server:', message);
|
|
156
|
+
|
|
157
|
+
const response = await fetch(url, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
...this.headers,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
clientId: this.clientId,
|
|
165
|
+
message,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorText = await response.text();
|
|
171
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('✅ Message sent successfully');
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Failed to send message:', error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Close the transport
|
|
183
|
+
*/
|
|
184
|
+
async close(): Promise<void> {
|
|
185
|
+
console.log('🛑 Closing HTTP transport...');
|
|
186
|
+
|
|
187
|
+
if (this.eventSource) {
|
|
188
|
+
this.eventSource.close();
|
|
189
|
+
this.eventSource = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.isConnected = false;
|
|
193
|
+
|
|
194
|
+
if (this.closeHandler) {
|
|
195
|
+
this.closeHandler();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('✅ HTTP transport closed');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Set handler for incoming messages
|
|
203
|
+
*/
|
|
204
|
+
onmessage = (handler: (message: JSONRPCMessage) => Promise<void>): void => {
|
|
205
|
+
this.messageHandler = handler;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Set handler for connection close
|
|
210
|
+
*/
|
|
211
|
+
onclose = (handler: () => void): void => {
|
|
212
|
+
this.closeHandler = handler;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set handler for errors
|
|
217
|
+
*/
|
|
218
|
+
onerror = (handler: (error: Error) => void): void => {
|
|
219
|
+
this.errorHandler = handler;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|