nitrostack 1.0.65 → 1.0.67
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 +3 -2
- 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
- package/templates/typescript-oauth/AI_AGENT_CLI_REFERENCE.md +0 -701
- package/templates/typescript-oauth/AI_AGENT_SDK_REFERENCE.md +0 -1260
- package/templates/typescript-oauth/package-lock.json +0 -4253
- package/templates/typescript-pizzaz/IMPLEMENTATION.md +0 -98
- package/templates/typescript-starter/AI_AGENT_CLI_REFERENCE.md +0 -701
- package/templates/typescript-starter/AI_AGENT_SDK_REFERENCE.md +0 -1260
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Log Manager for MCP Server
|
|
2
|
+
// Captures stdout/stderr from MCP server process and streams to clients
|
|
3
|
+
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
timestamp: number;
|
|
6
|
+
level: 'info' | 'error' | 'warn' | 'debug';
|
|
7
|
+
message: string;
|
|
8
|
+
source: 'stdout' | 'stderr' | 'system';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class LogManager {
|
|
12
|
+
private logs: LogEntry[] = [];
|
|
13
|
+
private maxLogs = 1000; // Keep last 1000 logs in memory
|
|
14
|
+
private listeners: Set<(log: LogEntry) => void> = new Set();
|
|
15
|
+
|
|
16
|
+
addLog(message: string, level: LogEntry['level'] = 'info', source: LogEntry['source'] = 'system') {
|
|
17
|
+
const entry: LogEntry = {
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
level,
|
|
20
|
+
message: message.trim(),
|
|
21
|
+
source,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.logs.push(entry);
|
|
25
|
+
|
|
26
|
+
// Keep only last maxLogs entries
|
|
27
|
+
if (this.logs.length > this.maxLogs) {
|
|
28
|
+
this.logs = this.logs.slice(-this.maxLogs);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Notify all listeners
|
|
32
|
+
this.listeners.forEach(listener => {
|
|
33
|
+
try {
|
|
34
|
+
listener(entry);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error in log listener:', error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getLogs(limit?: number): LogEntry[] {
|
|
42
|
+
if (limit) {
|
|
43
|
+
return this.logs.slice(-limit);
|
|
44
|
+
}
|
|
45
|
+
return [...this.logs];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clearLogs() {
|
|
49
|
+
this.logs = [];
|
|
50
|
+
this.addLog('Logs cleared', 'info', 'system');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
subscribe(listener: (log: LogEntry) => void): () => void {
|
|
54
|
+
this.listeners.add(listener);
|
|
55
|
+
return () => {
|
|
56
|
+
this.listeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getLogCount(): number {
|
|
61
|
+
return this.logs.length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Global singleton
|
|
66
|
+
declare global {
|
|
67
|
+
var __logManager: LogManager | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getLogManager(): LogManager {
|
|
71
|
+
if (!global.__logManager) {
|
|
72
|
+
global.__logManager = new LogManager();
|
|
73
|
+
}
|
|
74
|
+
return global.__logManager;
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// MCP Client for Studio
|
|
2
|
+
// Communicates with MCP server via stdio or HTTP
|
|
3
|
+
|
|
4
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
import { HttpClientTransport } from '@/lib/http-client-transport';
|
|
8
|
+
import { getLogManager } from '@/lib/log-manager';
|
|
9
|
+
|
|
10
|
+
export type TransportType = 'stdio' | 'http';
|
|
11
|
+
|
|
12
|
+
export interface StdioClientConfig {
|
|
13
|
+
type: 'stdio';
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HttpClientConfig {
|
|
21
|
+
type: 'http';
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
basePath?: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type McpClientConfig = StdioClientConfig | HttpClientConfig;
|
|
28
|
+
|
|
29
|
+
export class McpClient {
|
|
30
|
+
private client: Client | null = null;
|
|
31
|
+
private transport: StdioClientTransport | HttpClientTransport | null = null;
|
|
32
|
+
private transportType: TransportType | null = null;
|
|
33
|
+
private childProcess: ChildProcess | null = null;
|
|
34
|
+
|
|
35
|
+
async connect(config: McpClientConfig): Promise<void> {
|
|
36
|
+
if (this.client && this.transport) {
|
|
37
|
+
console.log('⚠️ Already connected, reusing existing connection');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Clean up any existing connections
|
|
42
|
+
if (this.client || this.transport) {
|
|
43
|
+
console.log('🧹 Cleaning up stale connection...');
|
|
44
|
+
try {
|
|
45
|
+
if (this.client) await this.client.close();
|
|
46
|
+
if (this.transport) await this.transport.close();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Ignore cleanup errors
|
|
49
|
+
}
|
|
50
|
+
this.client = null;
|
|
51
|
+
this.transport = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.transportType = config.type;
|
|
55
|
+
|
|
56
|
+
// Create appropriate transport based on type
|
|
57
|
+
if (config.type === 'stdio') {
|
|
58
|
+
console.log('🚀 Connecting to MCP server via STDIO:', config.command, config.args);
|
|
59
|
+
console.log('📝 Environment vars:', Object.keys(config.env || {}).join(', '));
|
|
60
|
+
|
|
61
|
+
const logManager = getLogManager();
|
|
62
|
+
logManager.addLog(`Starting MCP server: ${config.command} ${config.args.join(' ')}`, 'info', 'system');
|
|
63
|
+
|
|
64
|
+
// Create STDIO transport (it will spawn the process internally)
|
|
65
|
+
this.transport = new StdioClientTransport({
|
|
66
|
+
command: config.command,
|
|
67
|
+
args: config.args,
|
|
68
|
+
env: { ...process.env, ...config.env },
|
|
69
|
+
cwd: config.cwd,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Access the child process from the transport to capture logs
|
|
73
|
+
// The transport stores the process in a private property, so we need to access it
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
const transport = this.transport as any;
|
|
76
|
+
if (transport._process || transport.process) {
|
|
77
|
+
this.childProcess = transport._process || transport.process;
|
|
78
|
+
|
|
79
|
+
// Capture stdout
|
|
80
|
+
this.childProcess?.stdout?.on('data', (data: Buffer) => {
|
|
81
|
+
const message = data.toString();
|
|
82
|
+
logManager.addLog(message, 'info', 'stdout');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Capture stderr
|
|
86
|
+
this.childProcess?.stderr?.on('data', (data: Buffer) => {
|
|
87
|
+
const message = data.toString();
|
|
88
|
+
logManager.addLog(message, 'error', 'stderr');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Handle process errors
|
|
92
|
+
this.childProcess?.on('error', (error: Error) => {
|
|
93
|
+
logManager.addLog(`Process error: ${error.message}`, 'error', 'system');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Handle process exit
|
|
97
|
+
this.childProcess?.on('exit', (code: number | null, signal: string | null) => {
|
|
98
|
+
if (code !== null) {
|
|
99
|
+
logManager.addLog(`MCP server exited with code ${code}`, code === 0 ? 'info' : 'error', 'system');
|
|
100
|
+
} else if (signal) {
|
|
101
|
+
logManager.addLog(`MCP server killed with signal ${signal}`, 'warn', 'system');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}, 100); // Small delay to ensure transport has spawned the process
|
|
106
|
+
} else if (config.type === 'http') {
|
|
107
|
+
console.log('🌐 Connecting to MCP server via HTTP:', config.baseUrl);
|
|
108
|
+
|
|
109
|
+
// Create HTTP transport
|
|
110
|
+
this.transport = new HttpClientTransport({
|
|
111
|
+
baseUrl: config.baseUrl,
|
|
112
|
+
basePath: config.basePath,
|
|
113
|
+
headers: config.headers,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Start the HTTP transport (establish SSE connection)
|
|
117
|
+
await this.transport.start();
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(`Unknown transport type: ${(config as any).type}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create client
|
|
123
|
+
this.client = new Client(
|
|
124
|
+
{
|
|
125
|
+
name: 'nitrostack-studio',
|
|
126
|
+
version: '3.1.0',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
capabilities: {
|
|
130
|
+
sampling: {},
|
|
131
|
+
roots: { listChanged: true },
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Handle connection errors
|
|
137
|
+
this.client.onerror = (error) => {
|
|
138
|
+
console.error('❌ MCP Client error:', error);
|
|
139
|
+
this.client = null;
|
|
140
|
+
this.transport = null;
|
|
141
|
+
this.transportType = null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.client.onclose = () => {
|
|
145
|
+
console.log('🔌 MCP Client connection closed');
|
|
146
|
+
this.client = null;
|
|
147
|
+
this.transport = null;
|
|
148
|
+
this.transportType = null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Connect
|
|
152
|
+
try {
|
|
153
|
+
await this.client.connect(this.transport);
|
|
154
|
+
console.log(`✅ MCP Client connected and ready (${config.type.toUpperCase()} transport)`);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('❌ Failed to connect MCP client:', error);
|
|
157
|
+
this.client = null;
|
|
158
|
+
this.transport = null;
|
|
159
|
+
this.transportType = null;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async disconnect(): Promise<void> {
|
|
165
|
+
console.log('🛑 Disconnecting MCP client...');
|
|
166
|
+
|
|
167
|
+
const logManager = getLogManager();
|
|
168
|
+
|
|
169
|
+
if (this.client) {
|
|
170
|
+
await this.client.close();
|
|
171
|
+
this.client = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.transport) {
|
|
175
|
+
await this.transport.close();
|
|
176
|
+
this.transport = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.childProcess) {
|
|
180
|
+
logManager.addLog('Stopping MCP server process', 'info', 'system');
|
|
181
|
+
this.childProcess.kill('SIGTERM');
|
|
182
|
+
this.childProcess = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.transportType = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isConnected(): boolean {
|
|
189
|
+
return this.client !== null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getTransportType(): TransportType | null {
|
|
193
|
+
return this.transportType;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async listTools() {
|
|
197
|
+
if (!this.client) throw new Error('Not connected');
|
|
198
|
+
return await this.client.listTools();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async callTool(name: string, args: any) {
|
|
202
|
+
if (!this.client) throw new Error('Not connected');
|
|
203
|
+
return await this.client.callTool({ name, arguments: args });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async listResources() {
|
|
207
|
+
if (!this.client) throw new Error('Not connected');
|
|
208
|
+
return await this.client.listResources();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async readResource(uri: string) {
|
|
212
|
+
if (!this.client) throw new Error('Not connected');
|
|
213
|
+
return await this.client.readResource({ uri });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async listPrompts() {
|
|
217
|
+
if (!this.client) throw new Error('Not connected');
|
|
218
|
+
return await this.client.listPrompts();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getPrompt(name: string, args: any) {
|
|
222
|
+
if (!this.client) throw new Error('Not connected');
|
|
223
|
+
return await this.client.getPrompt({ name, arguments: args });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async ping() {
|
|
227
|
+
if (!this.client) throw new Error('Not connected');
|
|
228
|
+
return await this.client.ping();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async listRoots() {
|
|
232
|
+
if (!this.client) throw new Error('Not connected');
|
|
233
|
+
return await this.client.listRoots();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async executeTool(name: string, args: any) {
|
|
237
|
+
return await this.callTool(name, args);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async createCompletion(params: any) {
|
|
241
|
+
if (!this.client) throw new Error('Not connected');
|
|
242
|
+
return await this.client.createCompletion(params);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Global singleton that persists across HMR
|
|
247
|
+
declare global {
|
|
248
|
+
var __mcpClient: McpClient | undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function getMcpClient(): McpClient {
|
|
252
|
+
if (!global.__mcpClient) {
|
|
253
|
+
console.log('🆕 Creating new MCP client instance');
|
|
254
|
+
global.__mcpClient = new McpClient();
|
|
255
|
+
}
|
|
256
|
+
return global.__mcpClient;
|
|
257
|
+
}
|
|
258
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type {
|
|
3
|
+
Tool,
|
|
4
|
+
Resource,
|
|
5
|
+
Prompt,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ConnectionStatus,
|
|
8
|
+
TabType,
|
|
9
|
+
PingResult,
|
|
10
|
+
HealthCheck,
|
|
11
|
+
OAuthState,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
interface StudioState {
|
|
15
|
+
// Connection
|
|
16
|
+
connection: ConnectionStatus;
|
|
17
|
+
setConnection: (status: ConnectionStatus) => void;
|
|
18
|
+
|
|
19
|
+
// Navigation
|
|
20
|
+
currentTab: TabType;
|
|
21
|
+
setCurrentTab: (tab: TabType) => void;
|
|
22
|
+
|
|
23
|
+
// Data
|
|
24
|
+
tools: Tool[];
|
|
25
|
+
setTools: (tools: Tool[]) => void;
|
|
26
|
+
|
|
27
|
+
resources: Resource[];
|
|
28
|
+
setResources: (resources: Resource[]) => void;
|
|
29
|
+
|
|
30
|
+
prompts: Prompt[];
|
|
31
|
+
setPrompts: (prompts: Prompt[]) => void;
|
|
32
|
+
|
|
33
|
+
// Chat
|
|
34
|
+
chatMessages: ChatMessage[];
|
|
35
|
+
addChatMessage: (message: ChatMessage) => void;
|
|
36
|
+
clearChat: () => void;
|
|
37
|
+
currentProvider: 'openai' | 'gemini';
|
|
38
|
+
setCurrentProvider: (provider: 'openai' | 'gemini') => void;
|
|
39
|
+
currentImage: { data: string; type: string; name: string } | null;
|
|
40
|
+
setCurrentImage: (image: { data: string; type: string; name: string } | null) => void;
|
|
41
|
+
|
|
42
|
+
// Auth
|
|
43
|
+
jwtToken: string | null;
|
|
44
|
+
setJwtToken: (token: string | null) => void;
|
|
45
|
+
|
|
46
|
+
apiKey: string | null;
|
|
47
|
+
setApiKey: (key: string | null) => void;
|
|
48
|
+
|
|
49
|
+
oauthState: OAuthState;
|
|
50
|
+
setOAuthState: (state: Partial<OAuthState>) => void;
|
|
51
|
+
|
|
52
|
+
// Ping
|
|
53
|
+
pingHistory: PingResult[];
|
|
54
|
+
addPingResult: (result: PingResult) => void;
|
|
55
|
+
|
|
56
|
+
// Health
|
|
57
|
+
healthChecks: HealthCheck[];
|
|
58
|
+
setHealthChecks: (checks: HealthCheck[]) => void;
|
|
59
|
+
|
|
60
|
+
// Modal
|
|
61
|
+
enlargeModal: {
|
|
62
|
+
open: boolean;
|
|
63
|
+
type: 'tool' | 'resource' | null;
|
|
64
|
+
item: any;
|
|
65
|
+
};
|
|
66
|
+
openEnlargeModal: (type: 'tool' | 'resource', item: any) => void;
|
|
67
|
+
closeEnlargeModal: () => void;
|
|
68
|
+
|
|
69
|
+
// Loading states
|
|
70
|
+
loading: {
|
|
71
|
+
tools: boolean;
|
|
72
|
+
resources: boolean;
|
|
73
|
+
prompts: boolean;
|
|
74
|
+
chat: boolean;
|
|
75
|
+
};
|
|
76
|
+
setLoading: (key: keyof StudioState['loading'], value: boolean) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const useStudioStore = create<StudioState>((set) => ({
|
|
80
|
+
// Connection
|
|
81
|
+
connection: { connected: false, status: 'connecting' },
|
|
82
|
+
setConnection: (connection) => set({ connection }),
|
|
83
|
+
|
|
84
|
+
// Navigation
|
|
85
|
+
currentTab: 'tools',
|
|
86
|
+
setCurrentTab: (currentTab) => set({ currentTab }),
|
|
87
|
+
|
|
88
|
+
// Data
|
|
89
|
+
tools: [],
|
|
90
|
+
setTools: (tools) => set({ tools }),
|
|
91
|
+
|
|
92
|
+
resources: [],
|
|
93
|
+
setResources: (resources) => set({ resources }),
|
|
94
|
+
|
|
95
|
+
prompts: [],
|
|
96
|
+
setPrompts: (prompts) => set({ prompts }),
|
|
97
|
+
|
|
98
|
+
// Chat
|
|
99
|
+
chatMessages: [],
|
|
100
|
+
addChatMessage: (message) =>
|
|
101
|
+
set((state) => ({ chatMessages: [...state.chatMessages, message] })),
|
|
102
|
+
clearChat: () => set({ chatMessages: [] }),
|
|
103
|
+
currentProvider: 'gemini',
|
|
104
|
+
setCurrentProvider: (currentProvider) => set({ currentProvider }),
|
|
105
|
+
currentImage: null,
|
|
106
|
+
setCurrentImage: (currentImage) => set({ currentImage }),
|
|
107
|
+
|
|
108
|
+
// Auth
|
|
109
|
+
jwtToken: typeof window !== 'undefined' ? localStorage.getItem('mcp_jwt_token') : null,
|
|
110
|
+
setJwtToken: (jwtToken) => {
|
|
111
|
+
if (typeof window !== 'undefined') {
|
|
112
|
+
if (jwtToken) {
|
|
113
|
+
localStorage.setItem('mcp_jwt_token', jwtToken);
|
|
114
|
+
} else {
|
|
115
|
+
localStorage.removeItem('mcp_jwt_token');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
set({ jwtToken });
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
apiKey: typeof window !== 'undefined' ? localStorage.getItem('mcp_api_key') : null,
|
|
122
|
+
setApiKey: (apiKey) => {
|
|
123
|
+
if (typeof window !== 'undefined') {
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
localStorage.setItem('mcp_api_key', apiKey);
|
|
126
|
+
} else {
|
|
127
|
+
localStorage.removeItem('mcp_api_key');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
set({ apiKey });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
oauthState: typeof window !== 'undefined'
|
|
134
|
+
? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
|
|
135
|
+
authServerUrl: null,
|
|
136
|
+
resourceMetadata: null,
|
|
137
|
+
authServerMetadata: null,
|
|
138
|
+
clientRegistration: null,
|
|
139
|
+
selectedScopes: [],
|
|
140
|
+
currentToken: null,
|
|
141
|
+
}
|
|
142
|
+
: {
|
|
143
|
+
authServerUrl: null,
|
|
144
|
+
resourceMetadata: null,
|
|
145
|
+
authServerMetadata: null,
|
|
146
|
+
clientRegistration: null,
|
|
147
|
+
selectedScopes: [],
|
|
148
|
+
currentToken: null,
|
|
149
|
+
},
|
|
150
|
+
setOAuthState: (newState) => {
|
|
151
|
+
const updatedState = (state: any) => {
|
|
152
|
+
const newOAuthState = { ...state.oauthState, ...newState };
|
|
153
|
+
|
|
154
|
+
// Persist to localStorage
|
|
155
|
+
if (typeof window !== 'undefined') {
|
|
156
|
+
localStorage.setItem('mcp_oauth_state', JSON.stringify(newOAuthState));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { oauthState: newOAuthState };
|
|
160
|
+
};
|
|
161
|
+
set(updatedState);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Ping
|
|
165
|
+
pingHistory: [],
|
|
166
|
+
addPingResult: (result) =>
|
|
167
|
+
set((state) => ({
|
|
168
|
+
pingHistory: [result, ...state.pingHistory].slice(0, 10),
|
|
169
|
+
})),
|
|
170
|
+
|
|
171
|
+
// Health
|
|
172
|
+
healthChecks: [],
|
|
173
|
+
setHealthChecks: (healthChecks) => set({ healthChecks }),
|
|
174
|
+
|
|
175
|
+
// Modal
|
|
176
|
+
enlargeModal: { open: false, type: null, item: null },
|
|
177
|
+
openEnlargeModal: (type, item) =>
|
|
178
|
+
set({ enlargeModal: { open: true, type, item } }),
|
|
179
|
+
closeEnlargeModal: () =>
|
|
180
|
+
set({ enlargeModal: { open: false, type: null, item: null } }),
|
|
181
|
+
|
|
182
|
+
// Loading
|
|
183
|
+
loading: {
|
|
184
|
+
tools: false,
|
|
185
|
+
resources: false,
|
|
186
|
+
prompts: false,
|
|
187
|
+
chat: false,
|
|
188
|
+
},
|
|
189
|
+
setLoading: (key, value) =>
|
|
190
|
+
set((state) => ({ loading: { ...state.loading, [key]: value } })),
|
|
191
|
+
}));
|
|
192
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
type Theme = 'dark' | 'light';
|
|
6
|
+
|
|
7
|
+
type ThemeContextType = {
|
|
8
|
+
theme: Theme;
|
|
9
|
+
toggleTheme: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const [theme, setTheme] = useState<Theme>('dark');
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Load theme from localStorage
|
|
19
|
+
const stored = localStorage.getItem('theme') as Theme;
|
|
20
|
+
if (stored) {
|
|
21
|
+
setTheme(stored);
|
|
22
|
+
document.documentElement.classList.toggle('dark', stored === 'dark');
|
|
23
|
+
} else {
|
|
24
|
+
// Default to dark theme
|
|
25
|
+
document.documentElement.classList.add('dark');
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const toggleTheme = () => {
|
|
30
|
+
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
|
31
|
+
setTheme(newTheme);
|
|
32
|
+
localStorage.setItem('theme', newTheme);
|
|
33
|
+
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
38
|
+
{children}
|
|
39
|
+
</ThemeContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useTheme() {
|
|
44
|
+
const context = useContext(ThemeContext);
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error('useTheme must be used within ThemeProvider');
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// NitroStack Studio Type Definitions
|
|
2
|
+
|
|
3
|
+
export interface Tool {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema?: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties?: Record<string, any>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
};
|
|
11
|
+
examples?: {
|
|
12
|
+
request?: any;
|
|
13
|
+
response?: any;
|
|
14
|
+
};
|
|
15
|
+
widget?: {
|
|
16
|
+
route: string;
|
|
17
|
+
};
|
|
18
|
+
outputTemplate?: string;
|
|
19
|
+
_meta?: {
|
|
20
|
+
'openai/outputTemplate'?: string;
|
|
21
|
+
'ui/template'?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Resource {
|
|
27
|
+
name: string;
|
|
28
|
+
uri: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
examples?: {
|
|
32
|
+
response?: any;
|
|
33
|
+
};
|
|
34
|
+
_meta?: Record<string, any>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Prompt {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
arguments?: Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
required?: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Root {
|
|
48
|
+
uri: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChatMessage {
|
|
53
|
+
role: 'user' | 'assistant' | 'tool' | 'system';
|
|
54
|
+
content: string;
|
|
55
|
+
toolCalls?: ToolCall[];
|
|
56
|
+
toolCallId?: string;
|
|
57
|
+
image?: {
|
|
58
|
+
data: string;
|
|
59
|
+
type: string;
|
|
60
|
+
name: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ToolCall {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
arguments: any;
|
|
68
|
+
result?: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PingResult {
|
|
72
|
+
time: Date;
|
|
73
|
+
latency: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ConnectionStatus {
|
|
77
|
+
connected: boolean;
|
|
78
|
+
status: 'connected' | 'connecting' | 'disconnected';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface HealthCheck {
|
|
82
|
+
name: string;
|
|
83
|
+
status: 'up' | 'down' | 'degraded';
|
|
84
|
+
lastCheck: Date;
|
|
85
|
+
message?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface OAuthState {
|
|
89
|
+
authServerUrl: string | null;
|
|
90
|
+
resourceMetadata: any;
|
|
91
|
+
authServerMetadata: any;
|
|
92
|
+
clientRegistration: any;
|
|
93
|
+
selectedScopes: string[];
|
|
94
|
+
currentToken: any;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type TabType =
|
|
98
|
+
| 'tools'
|
|
99
|
+
| 'chat'
|
|
100
|
+
| 'resources'
|
|
101
|
+
| 'prompts'
|
|
102
|
+
| 'auth'
|
|
103
|
+
| 'health'
|
|
104
|
+
| 'ping'
|
|
105
|
+
| 'sampling'
|
|
106
|
+
| 'roots';
|
|
107
|
+
|