nitrostack 1.0.70 → 1.0.71

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitrostack",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
4
4
  "description": "NitroStack - Build powerful MCP servers with TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -14,8 +14,8 @@ export async function POST(request: NextRequest) {
14
14
  mcpApiKey?: string; // MCP server API key
15
15
  };
16
16
 
17
- console.log('Received chat request:', {
18
- provider,
17
+ console.log('Received chat request:', {
18
+ provider,
19
19
  messagesCount: messages?.length,
20
20
  messages: JSON.stringify(messages),
21
21
  hasApiKey: !!apiKey,
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
30
30
  { status: 400 }
31
31
  );
32
32
  }
33
-
33
+
34
34
  if (messages.length === 0) {
35
35
  return NextResponse.json(
36
36
  { error: 'Messages array is empty' },
@@ -46,13 +46,13 @@ export async function POST(request: NextRequest) {
46
46
  description: tool.description || '',
47
47
  inputSchema: tool.inputSchema || {},
48
48
  })) || [];
49
-
49
+
50
50
  // Add synthetic tools for prompts and resources
51
51
  const promptsList = await client.listPrompts().catch(() => ({ prompts: [] }));
52
52
  const resourcesList = await client.listResources().catch(() => ({ resources: [] }));
53
-
53
+
54
54
  const syntheticTools = [];
55
-
55
+
56
56
  // Add a tool to list available prompts
57
57
  if (promptsList.prompts && promptsList.prompts.length > 0) {
58
58
  syntheticTools.push({
@@ -63,7 +63,7 @@ export async function POST(request: NextRequest) {
63
63
  properties: {},
64
64
  },
65
65
  });
66
-
66
+
67
67
  // Add a tool to execute prompts
68
68
  syntheticTools.push({
69
69
  name: 'execute_prompt',
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
84
84
  },
85
85
  });
86
86
  }
87
-
87
+
88
88
  // Add a tool to list available resources
89
89
  if (resourcesList.resources && resourcesList.resources.length > 0) {
90
90
  syntheticTools.push({
@@ -95,7 +95,7 @@ export async function POST(request: NextRequest) {
95
95
  properties: {},
96
96
  },
97
97
  });
98
-
98
+
99
99
  // Add a tool to read resources
100
100
  syntheticTools.push({
101
101
  name: 'read_resource',
@@ -112,7 +112,7 @@ export async function POST(request: NextRequest) {
112
112
  },
113
113
  });
114
114
  }
115
-
115
+
116
116
  const tools = [...mcpTools, ...syntheticTools];
117
117
 
118
118
  // Call LLM
@@ -126,7 +126,7 @@ export async function POST(request: NextRequest) {
126
126
  try {
127
127
  let result: any;
128
128
  let toolContent = '';
129
-
129
+
130
130
  // Handle synthetic tools
131
131
  if (toolCall.name === 'list_prompts') {
132
132
  const prompts = await client.listPrompts();
@@ -172,21 +172,37 @@ export async function POST(request: NextRequest) {
172
172
  // Regular MCP tool execution
173
173
  // Inject auth tokens into tool arguments if available
174
174
  const toolArgs = { ...toolCall.arguments };
175
+
176
+ // Check if we have a file in the last user message and if this is a convert_temperature tool
177
+ if (toolCall.name === 'convert_temperature') {
178
+ const lastUserMessage = messages.slice().reverse().find(m => m.role === 'user');
179
+ if (lastUserMessage && lastUserMessage.file) {
180
+ // Inject file content if not already provided by LLM (or overwrite it to be safe)
181
+ console.log('📂 Injecting file content into convert_temperature tool call');
182
+ toolArgs.file_content = lastUserMessage.file.data;
183
+ toolArgs.file_type = lastUserMessage.file.type;
184
+
185
+ if (!toolArgs.file_name) {
186
+ toolArgs.file_name = lastUserMessage.file.name;
187
+ }
188
+ }
189
+ }
190
+
175
191
  if (jwtToken || mcpApiKey) {
176
192
  toolArgs._meta = {
177
193
  ...(toolArgs._meta || {}),
178
194
  };
179
-
195
+
180
196
  if (jwtToken) {
181
197
  toolArgs._meta._jwt = jwtToken;
182
198
  toolArgs._meta.authorization = `Bearer ${jwtToken}`;
183
199
  }
184
-
200
+
185
201
  if (mcpApiKey) {
186
202
  toolArgs._meta.apiKey = mcpApiKey;
187
203
  toolArgs._meta['x-api-key'] = mcpApiKey;
188
204
  }
189
-
205
+
190
206
  console.log(`🔐 Executing tool "${toolCall.name}" with auth:`, {
191
207
  hasJwt: !!jwtToken,
192
208
  hasMcpApiKey: !!mcpApiKey,
@@ -30,8 +30,8 @@ export default function ChatPage() {
30
30
  clearChat,
31
31
  currentProvider,
32
32
  setCurrentProvider,
33
- currentImage,
34
- setCurrentImage,
33
+ currentFile,
34
+ setCurrentFile,
35
35
  tools,
36
36
  setTools,
37
37
  } = useStudioStore();
@@ -362,7 +362,7 @@ export default function ChatPage() {
362
362
  }
363
363
  };
364
364
 
365
- const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
365
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
366
366
  const file = e.target.files?.[0];
367
367
  if (!file) return;
368
368
 
@@ -373,7 +373,7 @@ export default function ChatPage() {
373
373
 
374
374
  const reader = new FileReader();
375
375
  reader.onload = (event) => {
376
- setCurrentImage({
376
+ setCurrentFile({
377
377
  data: event.target?.result as string,
378
378
  type: file.type,
379
379
  name: file.name,
@@ -383,7 +383,7 @@ export default function ChatPage() {
383
383
  };
384
384
 
385
385
  const handleSend = async () => {
386
- if (!inputValue.trim() && !currentImage) return;
386
+ if (!inputValue.trim() && !currentFile) return;
387
387
 
388
388
  const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
389
389
  if (!apiKey) {
@@ -397,13 +397,13 @@ export default function ChatPage() {
397
397
  content: inputValue,
398
398
  };
399
399
 
400
- if (currentImage) {
401
- userMessage.image = currentImage;
400
+ if (currentFile) {
401
+ userMessage.file = currentFile;
402
402
  }
403
403
 
404
404
  addChatMessage(userMessage);
405
405
  setInputValue('');
406
- setCurrentImage(null);
406
+ setCurrentFile(null);
407
407
  setLoading(true);
408
408
 
409
409
  try {
@@ -425,9 +425,9 @@ export default function ChatPage() {
425
425
  }
426
426
 
427
427
  // Skip image property for now (not supported by OpenAI chat completions)
428
- // if (msg.image) {
429
- // cleaned.image = msg.image;
430
- // }
428
+ if (msg.file) {
429
+ cleaned.file = msg.file;
430
+ }
431
431
 
432
432
  return cleaned;
433
433
  });
@@ -536,7 +536,7 @@ export default function ChatPage() {
536
536
  }
537
537
  };
538
538
 
539
- const continueChatWithToolResults = async (apiKey: string, messages?: Message[]) => {
539
+ const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[]) => {
540
540
  try {
541
541
  // Use provided messages or fall back to store (for recursive calls)
542
542
  const messagesToUse = messages || chatMessages;
@@ -579,7 +579,7 @@ export default function ChatPage() {
579
579
 
580
580
  // Recursive tool calls
581
581
  if (response.toolCalls && response.toolResults) {
582
- const newToolResults: Message[] = [];
582
+ const newToolResults: ChatMessage[] = [];
583
583
  for (const result of response.toolResults) {
584
584
  addChatMessage(result);
585
585
  newToolResults.push(result);
@@ -908,19 +908,25 @@ export default function ChatPage() {
908
908
  {/* ChatGPT-style Input Area - Fixed at bottom */}
909
909
  <div className="sticky bottom-0 border-t border-border/50 bg-background/95 backdrop-blur-md shadow-[0_-2px_10px_rgba(0,0,0,0.1)]">
910
910
  <div className="max-w-5xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
911
- {currentImage && (
911
+ {currentFile && (
912
912
  <div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
913
- <img
914
- src={currentImage.data}
915
- alt={currentImage.name}
916
- className="w-20 h-20 object-cover rounded-lg border border-border"
917
- />
913
+ {currentFile.type.startsWith('image/') ? (
914
+ <img
915
+ src={currentFile.data}
916
+ alt={currentFile.name}
917
+ className="w-20 h-20 object-cover rounded-lg border border-border"
918
+ />
919
+ ) : (
920
+ <div className="w-20 h-20 rounded-lg border border-border bg-muted flex items-center justify-center">
921
+ <FileText className="w-8 h-8 text-muted-foreground" />
922
+ </div>
923
+ )}
918
924
  <div className="flex-1 min-w-0">
919
- <p className="text-sm font-medium text-foreground truncate">{currentImage.name}</p>
920
- <p className="text-xs text-muted-foreground">{currentImage.type}</p>
925
+ <p className="text-sm font-medium text-foreground truncate">{currentFile.name}</p>
926
+ <p className="text-xs text-muted-foreground">{currentFile.type}</p>
921
927
  </div>
922
928
  <button
923
- onClick={() => setCurrentImage(null)}
929
+ onClick={() => setCurrentFile(null)}
924
930
  className="w-7 h-7 rounded-lg flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
925
931
  >
926
932
  <X className="w-4 h-4" />
@@ -931,14 +937,14 @@ export default function ChatPage() {
931
937
  <input
932
938
  type="file"
933
939
  ref={fileInputRef}
934
- onChange={handleImageUpload}
935
- accept="image/*"
940
+ onChange={handleFileUpload}
941
+ accept="image/*,.pdf,.txt,.md,.json,.csv,.docx"
936
942
  className="hidden"
937
943
  />
938
944
  <button
939
945
  onClick={() => fileInputRef.current?.click()}
940
946
  className="h-11 w-11 rounded-xl flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
941
- title="Upload image"
947
+ title="Upload file"
942
948
  >
943
949
  <ImageIcon className="w-5 h-5" />
944
950
  </button>
@@ -966,7 +972,7 @@ export default function ChatPage() {
966
972
  </div>
967
973
  <button
968
974
  onClick={handleSend}
969
- disabled={loading || (!inputValue.trim() && !currentImage)}
975
+ disabled={loading || (!inputValue.trim() && !currentFile)}
970
976
  className="h-11 w-11 rounded-xl flex items-center justify-center bg-gradient-to-br from-primary to-amber-500 text-white shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all flex-shrink-0 hover:scale-105 active:scale-95"
971
977
  title="Send message (Enter)"
972
978
  >
@@ -1097,14 +1103,26 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
1097
1103
 
1098
1104
  {/* Message Content */}
1099
1105
  <div className="flex-1 min-w-0">
1100
- {/* Image if present */}
1101
- {message.image && (
1102
- <div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm">
1103
- <img
1104
- src={message.image.data}
1105
- alt={message.image.name}
1106
- className="max-w-full"
1107
- />
1106
+ {/* File if present */}
1107
+ {message.file && (
1108
+ <div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm max-w-sm">
1109
+ {message.file.type.startsWith('image/') ? (
1110
+ <img
1111
+ src={message.file.data}
1112
+ alt={message.file.name}
1113
+ className="max-w-full"
1114
+ />
1115
+ ) : (
1116
+ <div className="p-4 bg-muted/30 flex items-center gap-3">
1117
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
1118
+ <FileText className="w-5 h-5 text-primary" />
1119
+ </div>
1120
+ <div className="flex-1 min-w-0">
1121
+ <p className="text-sm font-medium text-foreground truncate">{message.file.name}</p>
1122
+ <p className="text-xs text-muted-foreground">{message.file.type}</p>
1123
+ </div>
1124
+ </div>
1125
+ )}
1108
1126
  </div>
1109
1127
  )}
1110
1128
 
@@ -9,6 +9,11 @@ export interface ChatMessage {
9
9
  toolCalls?: ToolCall[];
10
10
  toolCallId?: string; // For tool responses - the ID of the call being responded to
11
11
  toolName?: string; // For tool responses - the name of the tool (required by Gemini)
12
+ file?: {
13
+ data: string;
14
+ type: string;
15
+ name: string;
16
+ };
12
17
  }
13
18
 
14
19
  export interface ToolCall {
@@ -187,6 +192,53 @@ User: "list all resources"
187
192
  };
188
193
  }
189
194
 
195
+ if (msg.role === 'user' && msg.file) {
196
+ // Handle file attachments for OpenAI
197
+ const contentParts: any[] = [
198
+ { type: 'text', text: msg.content || ' ' } // Ensure some text exists
199
+ ];
200
+
201
+ if (msg.file.type.startsWith('image/')) {
202
+ contentParts.push({
203
+ type: 'image_url',
204
+ image_url: {
205
+ url: msg.file.data,
206
+ }
207
+ });
208
+ } else {
209
+ // For non-image files, append as text if it's a text file, or note unavailable
210
+ // OpenAI (without Vision/File Search) can't natively handle arbitrary files in chat completions
211
+ // So we append it as context if it looks like text
212
+ if (msg.file.type.startsWith('text/') || msg.file.name.endsWith('.txt') || msg.file.name.endsWith('.md') || msg.file.name.endsWith('.json')) {
213
+ // For text files, we might need to decode base64 if it's base64 encoded
214
+ // Assuming data is "data:mime;base64,..."
215
+ try {
216
+ const base64Content = msg.file.data.split(',')[1];
217
+ const textContent = Buffer.from(base64Content, 'base64').toString('utf-8');
218
+ contentParts.push({
219
+ type: 'text',
220
+ text: `\n\n[Attached File: ${msg.file.name}]\n${textContent}`
221
+ });
222
+ } catch (e) {
223
+ contentParts.push({
224
+ type: 'text',
225
+ text: `\n\n[Attached File: ${msg.file.name}] (Could not decode content)`
226
+ });
227
+ }
228
+ } else {
229
+ contentParts.push({
230
+ type: 'text',
231
+ text: `\n\n[Attached File: ${msg.file.name}] (Type: ${msg.file.type})`
232
+ });
233
+ }
234
+ }
235
+
236
+ return {
237
+ role: msg.role,
238
+ content: contentParts,
239
+ };
240
+ }
241
+
190
242
  return {
191
243
  role: msg.role,
192
244
  content: msg.content,
@@ -334,9 +386,60 @@ User: "list all resources"
334
386
  i++;
335
387
  } else {
336
388
  // Regular user or assistant message
389
+ const parts: any[] = [];
390
+
391
+ if (msg.content) {
392
+ parts.push({ text: msg.content });
393
+ }
394
+
395
+ if (msg.role === 'user' && msg.file) {
396
+ // Extract base64 and mime type
397
+ const matches = msg.file.data.match(/^data:([^;]+);base64,(.+)$/);
398
+ if (matches) {
399
+ const mimeType = matches[1];
400
+ const data = matches[2];
401
+
402
+ // Gemini supports: PDF, image/*, video/*, audio/*
403
+ const isSupported =
404
+ mimeType === 'application/pdf' ||
405
+ mimeType.startsWith('image/') ||
406
+ mimeType.startsWith('video/') ||
407
+ mimeType.startsWith('audio/');
408
+
409
+ if (isSupported) {
410
+ parts.push({
411
+ inlineData: {
412
+ mimeType: mimeType,
413
+ data: data
414
+ }
415
+ });
416
+ } else {
417
+ // For unsupported types (DOCX, TXT, JSON, etc.), try to decode and pass as text context
418
+ // This acts as a "poor man's" file processing for text-based formats
419
+ try {
420
+ const textContent = Buffer.from(data, 'base64').toString('utf-8');
421
+ // Clean up non-printable characters if it's a binary file like DOCX appearing as text
422
+ // Note: For real DOCX parsing we'd need a library, but for now we pass raw or decoded text
423
+ // If the user wants to process it with a tool, the *tool* will get the raw base64.
424
+ // Here we just want to avoid crashing Gemini.
425
+
426
+ parts.push({
427
+ text: `\n\n[Attached File: ${msg.file.name} (${mimeType})]\n(File content is available to tools via file_content parameter)`
428
+ });
429
+ } catch (e) {
430
+ parts.push({
431
+ text: `\n\n[Attached File: ${msg.file.name} (${mimeType})]\n(Content available to tools)`
432
+ });
433
+ }
434
+ }
435
+ } else {
436
+ console.warn('Could not parse file data URI:', msg.file.name);
437
+ }
438
+ }
439
+
337
440
  contents.push({
338
441
  role: msg.role === 'assistant' ? 'model' : 'user',
339
- parts: [{ text: msg.content }],
442
+ parts,
340
443
  });
341
444
  i++;
342
445
  }
@@ -36,8 +36,8 @@ interface StudioState {
36
36
  clearChat: () => void;
37
37
  currentProvider: 'openai' | 'gemini';
38
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;
39
+ currentFile: { data: string; type: string; name: string } | null;
40
+ setCurrentFile: (file: { data: string; type: string; name: string } | null) => void;
41
41
 
42
42
  // Auth
43
43
  jwtToken: string | null;
@@ -102,8 +102,8 @@ export const useStudioStore = create<StudioState>((set) => ({
102
102
  clearChat: () => set({ chatMessages: [] }),
103
103
  currentProvider: 'gemini',
104
104
  setCurrentProvider: (currentProvider) => set({ currentProvider }),
105
- currentImage: null,
106
- setCurrentImage: (currentImage) => set({ currentImage }),
105
+ currentFile: null,
106
+ setCurrentFile: (currentFile) => set({ currentFile }),
107
107
 
108
108
  // Auth
109
109
  jwtToken: typeof window !== 'undefined' ? localStorage.getItem('mcp_jwt_token') : null,
@@ -130,32 +130,32 @@ export const useStudioStore = create<StudioState>((set) => ({
130
130
  set({ apiKey });
131
131
  },
132
132
 
133
- oauthState: typeof window !== 'undefined'
133
+ oauthState: typeof window !== 'undefined'
134
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
- }
135
+ authServerUrl: null,
136
+ resourceMetadata: null,
137
+ authServerMetadata: null,
138
+ clientRegistration: null,
139
+ selectedScopes: [],
140
+ currentToken: null,
141
+ }
142
142
  : {
143
- authServerUrl: null,
144
- resourceMetadata: null,
145
- authServerMetadata: null,
146
- clientRegistration: null,
147
- selectedScopes: [],
148
- currentToken: null,
149
- },
143
+ authServerUrl: null,
144
+ resourceMetadata: null,
145
+ authServerMetadata: null,
146
+ clientRegistration: null,
147
+ selectedScopes: [],
148
+ currentToken: null,
149
+ },
150
150
  setOAuthState: (newState) => {
151
151
  const updatedState = (state: any) => {
152
152
  const newOAuthState = { ...state.oauthState, ...newState };
153
-
153
+
154
154
  // Persist to localStorage
155
155
  if (typeof window !== 'undefined') {
156
156
  localStorage.setItem('mcp_oauth_state', JSON.stringify(newOAuthState));
157
157
  }
158
-
158
+
159
159
  return { oauthState: newOAuthState };
160
160
  };
161
161
  set(updatedState);
@@ -54,7 +54,7 @@ export interface ChatMessage {
54
54
  content: string;
55
55
  toolCalls?: ToolCall[];
56
56
  toolCallId?: string;
57
- image?: {
57
+ file?: {
58
58
  data: string;
59
59
  type: string;
60
60
  name: string;