shreyask commited on
Commit
d89db9c
·
verified ·
1 Parent(s): e5e84f8

add mcp support

Browse files
package.json CHANGED
@@ -11,6 +11,7 @@
11
  },
12
  "dependencies": {
13
  "@huggingface/transformers": "^3.7.1",
 
14
  "@monaco-editor/react": "^4.7.0",
15
  "@tailwindcss/vite": "^4.1.11",
16
  "idb": "^8.0.3",
 
11
  },
12
  "dependencies": {
13
  "@huggingface/transformers": "^3.7.1",
14
+ "@modelcontextprotocol/sdk": "^1.17.3",
15
  "@monaco-editor/react": "^4.7.0",
16
  "@tailwindcss/vite": "^4.1.11",
17
  "idb": "^8.0.3",
src/App.tsx CHANGED
@@ -6,8 +6,18 @@ import React, {
6
  useMemo,
7
  } from "react";
8
  import { openDB, type IDBPDatabase } from "idb";
9
- import { Play, Plus, Zap, RotateCcw, Settings, X } from "lucide-react";
 
 
 
 
 
 
 
 
 
10
  import { useLLM } from "./hooks/useLLM";
 
11
 
12
  import type { Tool } from "./components/ToolItem";
13
 
@@ -31,14 +41,15 @@ import ToolCallIndicator from "./components/ToolCallIndicator";
31
  import ToolItem from "./components/ToolItem";
32
  import ResultBlock from "./components/ResultBlock";
33
  import ExamplePrompts from "./components/ExamplePrompts";
 
34
 
35
  import { LoadingScreen } from "./components/LoadingScreen";
36
 
37
  interface RenderInfo {
38
  call: string;
39
- result?: any;
40
  renderer?: string;
41
- input?: Record<string, any>;
42
  error?: string;
43
  }
44
 
@@ -71,7 +82,7 @@ async function getDB(): Promise<IDBPDatabase> {
71
 
72
  const App: React.FC = () => {
73
  const [systemPrompt, setSystemPrompt] = useState<string>(
74
- DEFAULT_SYSTEM_PROMPT,
75
  );
76
  const [isSystemPromptModalOpen, setIsSystemPromptModalOpen] =
77
  useState<boolean>(false);
@@ -82,10 +93,12 @@ const App: React.FC = () => {
82
  const [isGenerating, setIsGenerating] = useState<boolean>(false);
83
  const isMobile = useMemo(isMobileOrTablet, []);
84
  const [selectedModelId, setSelectedModelId] = useState<string>(
85
- isMobile ? "350M" : "1.2B",
86
  );
87
  const [isModelDropdownOpen, setIsModelDropdownOpen] =
88
  useState<boolean>(false);
 
 
89
  const chatContainerRef = useRef<HTMLDivElement>(null);
90
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
91
  const toolsContainerRef = useRef<HTMLDivElement>(null);
@@ -100,7 +113,15 @@ const App: React.FC = () => {
100
  clearPastKeyValues,
101
  } = useLLM(selectedModelId);
102
 
 
 
 
 
 
 
 
103
  const loadTools = useCallback(async (): Promise<void> => {
 
104
  const db = await getDB();
105
  const allTools: Tool[] = await db.getAll(STORE_NAME);
106
  if (allTools.length === 0) {
@@ -111,7 +132,7 @@ const App: React.FC = () => {
111
  code,
112
  enabled: true,
113
  isCollapsed: false,
114
- }),
115
  );
116
  const tx = db.transaction(STORE_NAME, "readwrite");
117
  await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
@@ -120,11 +141,30 @@ const App: React.FC = () => {
120
  } else {
121
  setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
122
  }
123
- }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  useEffect(() => {
126
  loadTools();
127
- }, [loadTools]);
 
 
 
 
128
 
129
  useEffect(() => {
130
  if (chatContainerRef.current) {
@@ -202,16 +242,16 @@ const App: React.FC = () => {
202
  const toggleToolCollapsed = (id: number): void => {
203
  setTools(
204
  tools.map((tool) =>
205
- tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool,
206
- ),
207
  );
208
  };
209
 
210
  const expandTool = (id: number): void => {
211
  setTools(
212
  tools.map((tool) =>
213
- tool.id === id ? { ...tool, isCollapsed: false } : tool,
214
- ),
215
  );
216
  };
217
 
@@ -238,21 +278,107 @@ const App: React.FC = () => {
238
  const toolToUse = tools.find((t) => t.name === name && t.enabled);
239
  if (!toolToUse) throw new Error(`Tool '${name}' not found or is disabled.`);
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
242
  const schema = generateSchemaFromCode(functionCode);
243
  const paramNames = Object.keys(schema.parameters.properties);
244
 
245
- const finalArgs: any[] = [];
246
  const requiredParams = schema.parameters.required || [];
247
 
248
  for (let i = 0; i < paramNames.length; ++i) {
249
  const paramName = paramNames[i];
250
  if (i < positionalArgs.length) {
251
  finalArgs.push(positionalArgs[i]);
252
- } else if (keywordArgs.hasOwnProperty(paramName)) {
253
  finalArgs.push(keywordArgs[paramName]);
254
  } else if (
255
- schema.parameters.properties[paramName].hasOwnProperty("default")
 
 
 
256
  ) {
257
  finalArgs.push(schema.parameters.properties[paramName].default);
258
  } else if (!requiredParams.includes(paramName)) {
@@ -265,12 +391,12 @@ const App: React.FC = () => {
265
  const bodyMatch = functionCode.match(/function[^{]+\{([\s\S]*)\}/);
266
  if (!bodyMatch) {
267
  throw new Error(
268
- "Could not parse function body. Ensure it's a standard `function` declaration.",
269
  );
270
  }
271
  const body = bodyMatch[1];
272
  const AsyncFunction = Object.getPrototypeOf(
273
- async function () {},
274
  ).constructor;
275
  const func = new AsyncFunction(...paramNames, body);
276
  const result = await func(...finalArgs);
@@ -278,7 +404,7 @@ const App: React.FC = () => {
278
  };
279
 
280
  const executeToolCalls = async (
281
- toolCallContent: string,
282
  ): Promise<RenderInfo[]> => {
283
  const toolCalls = extractPythonicCalls(toolCallContent);
284
  if (toolCalls.length === 0)
@@ -296,23 +422,31 @@ const App: React.FC = () => {
296
  ? extractFunctionAndRenderer(toolUsed.code)
297
  : { rendererCode: undefined };
298
 
 
 
 
 
 
 
299
  let parsedResult;
300
  try {
301
  parsedResult = JSON.parse(result);
 
302
  } catch {
303
  parsedResult = result;
 
304
  }
305
 
306
- let namedParams: Record<string, any> = Object.create(null);
307
  if (parsedCall && toolUsed) {
308
  const schema = generateSchemaFromCode(
309
- extractFunctionAndRenderer(toolUsed.code).functionCode,
310
  );
311
  const paramNames = Object.keys(schema.parameters.properties);
312
  namedParams = mapArgsToNamedParams(
313
  paramNames,
314
  parsedCall.positionalArgs,
315
- parsedCall.keywordArgs,
316
  );
317
  }
318
 
@@ -322,6 +456,15 @@ const App: React.FC = () => {
322
  renderer: rendererCode,
323
  input: namedParams,
324
  });
 
 
 
 
 
 
 
 
 
325
  } catch (error) {
326
  const errorMessage = getErrorMessage(error);
327
  results.push({ call, error: errorMessage });
@@ -334,7 +477,7 @@ const App: React.FC = () => {
334
  if (!input.trim() || !isReady) return;
335
 
336
  const userMessage: Message = { role: "user", content: input };
337
- let currentMessages: Message[] = [...messages, userMessage];
338
  setMessages(currentMessages);
339
  setInput("");
340
  setIsGenerating(true);
@@ -366,7 +509,7 @@ const App: React.FC = () => {
366
  };
367
  return updated;
368
  });
369
- },
370
  );
371
 
372
  currentMessages.push({ role: "assistant", content: response });
@@ -425,7 +568,7 @@ const App: React.FC = () => {
425
  console.error("Failed to save system prompt:", error);
426
  }
427
  },
428
- [],
429
  );
430
 
431
  const loadSelectedModel = useCallback(async (): Promise<void> => {
@@ -434,7 +577,7 @@ const App: React.FC = () => {
434
  } catch (error) {
435
  console.error("Failed to load model:", error);
436
  }
437
- }, [selectedModelId, loadModel]);
438
 
439
  const loadSelectedModelId = useCallback(async (): Promise<void> => {
440
  try {
@@ -484,7 +627,7 @@ const App: React.FC = () => {
484
  console.error("Failed to save selected model ID:", error);
485
  }
486
  },
487
- [],
488
  );
489
 
490
  useEffect(() => {
@@ -535,7 +678,7 @@ const App: React.FC = () => {
535
  };
536
  return updated;
537
  });
538
- },
539
  );
540
 
541
  currentMessages.push({ role: "assistant", content: response });
@@ -587,7 +730,11 @@ const App: React.FC = () => {
587
  />
588
  ) : (
589
  <div className="flex h-screen text-white">
590
- <div className="w-1/2 flex flex-col p-4">
 
 
 
 
591
  <div className="flex items-center justify-between mb-4">
592
  <div className="flex items-center gap-3">
593
  <h1 className="text-3xl font-bold text-gray-200">
@@ -618,6 +765,28 @@ const App: React.FC = () => {
618
  >
619
  <Settings size={16} />
620
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  </div>
622
  </div>
623
 
@@ -643,7 +812,7 @@ const App: React.FC = () => {
643
  );
644
  } else if (msg.role === "assistant") {
645
  const isToolCall = msg.content.includes(
646
- "<|tool_call_start|>",
647
  );
648
 
649
  if (isToolCall) {
@@ -652,7 +821,7 @@ const App: React.FC = () => {
652
  const hasError =
653
  isCompleted &&
654
  (nextMessage as ToolMessage).renderInfo.some(
655
- (info) => !!info.error,
656
  );
657
 
658
  return (
@@ -680,7 +849,7 @@ const App: React.FC = () => {
680
  } else if (msg.role === "tool") {
681
  const visibleToolResults = msg.renderInfo.filter(
682
  (info) =>
683
- info.error || (info.result != null && info.renderer),
684
  );
685
 
686
  if (visibleToolResults.length === 0) return null;
@@ -698,7 +867,9 @@ const App: React.FC = () => {
698
  <ResultBlock error={info.error} />
699
  ) : (
700
  <ToolResultRenderer
701
- result={info.result}
 
 
702
  rendererCode={info.renderer}
703
  input={info.input}
704
  />
@@ -745,35 +916,37 @@ const App: React.FC = () => {
745
  </div>
746
  </div>
747
 
748
- <div className="w-1/2 flex flex-col p-4 border-l border-gray-700">
749
- <div className="flex justify-between items-center mb-4">
750
- <h2 className="text-2xl font-bold text-teal-400">Tools</h2>
751
- <button
752
- onClick={addTool}
753
- className="flex items-center bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
 
 
 
 
 
 
 
 
754
  >
755
- <Plus size={16} className="mr-2" /> Add Tool
756
- </button>
757
- </div>
758
- <div
759
- ref={toolsContainerRef}
760
- className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto space-y-3"
761
- >
762
- {tools.map((tool) => (
763
- <ToolItem
764
- key={tool.id}
765
- tool={tool}
766
- onToggleEnabled={() => toggleToolEnabled(tool.id)}
767
- onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
768
- onExpand={() => expandTool(tool.id)}
769
- onDelete={() => deleteTool(tool.id)}
770
- onCodeChange={(newCode) =>
771
- handleToolCodeChange(tool.id, newCode)
772
- }
773
- />
774
- ))}
775
  </div>
776
- </div>
777
  </div>
778
  )}
779
 
@@ -819,6 +992,12 @@ const App: React.FC = () => {
819
  </div>
820
  </div>
821
  )}
 
 
 
 
 
 
822
  </div>
823
  );
824
  };
 
6
  useMemo,
7
  } from "react";
8
  import { openDB, type IDBPDatabase } from "idb";
9
+ import {
10
+ Play,
11
+ Plus,
12
+ Zap,
13
+ RotateCcw,
14
+ Settings,
15
+ X,
16
+ PanelRightClose,
17
+ PanelRightOpen,
18
+ } from "lucide-react";
19
  import { useLLM } from "./hooks/useLLM";
20
+ import { useMCP } from "./hooks/useMCP";
21
 
22
  import type { Tool } from "./components/ToolItem";
23
 
 
41
  import ToolItem from "./components/ToolItem";
42
  import ResultBlock from "./components/ResultBlock";
43
  import ExamplePrompts from "./components/ExamplePrompts";
44
+ import { MCPServerManager } from "./components/MCPServerManager";
45
 
46
  import { LoadingScreen } from "./components/LoadingScreen";
47
 
48
  interface RenderInfo {
49
  call: string;
50
+ result?: unknown;
51
  renderer?: string;
52
+ input?: Record<string, unknown>;
53
  error?: string;
54
  }
55
 
 
82
 
83
  const App: React.FC = () => {
84
  const [systemPrompt, setSystemPrompt] = useState<string>(
85
+ DEFAULT_SYSTEM_PROMPT
86
  );
87
  const [isSystemPromptModalOpen, setIsSystemPromptModalOpen] =
88
  useState<boolean>(false);
 
93
  const [isGenerating, setIsGenerating] = useState<boolean>(false);
94
  const isMobile = useMemo(isMobileOrTablet, []);
95
  const [selectedModelId, setSelectedModelId] = useState<string>(
96
+ isMobile ? "350M" : "1.2B"
97
  );
98
  const [isModelDropdownOpen, setIsModelDropdownOpen] =
99
  useState<boolean>(false);
100
+ const [isMCPManagerOpen, setIsMCPManagerOpen] = useState<boolean>(false);
101
+ const [isToolsPanelVisible, setIsToolsPanelVisible] = useState(false);
102
  const chatContainerRef = useRef<HTMLDivElement>(null);
103
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
104
  const toolsContainerRef = useRef<HTMLDivElement>(null);
 
113
  clearPastKeyValues,
114
  } = useLLM(selectedModelId);
115
 
116
+ // MCP integration
117
+ const {
118
+ getMCPToolsAsOriginalTools,
119
+ callMCPTool,
120
+ connectAll: connectAllMCPServers,
121
+ } = useMCP();
122
+
123
  const loadTools = useCallback(async (): Promise<void> => {
124
+ console.log("loadTools called - this should only happen on startup");
125
  const db = await getDB();
126
  const allTools: Tool[] = await db.getAll(STORE_NAME);
127
  if (allTools.length === 0) {
 
132
  code,
133
  enabled: true,
134
  isCollapsed: false,
135
+ })
136
  );
137
  const tx = db.transaction(STORE_NAME, "readwrite");
138
  await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
 
141
  } else {
142
  setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
143
  }
144
+ }, []); // Remove the dependency that was causing the loop
145
+
146
+ // Separate effect for MCP tools to avoid dependency cycles
147
+ useEffect(() => {
148
+ console.log(
149
+ "MCP tools effect triggered - checking if this happens frequently"
150
+ );
151
+ const mcpTools = getMCPToolsAsOriginalTools();
152
+ setTools((prevTools) => {
153
+ // Remove old MCP tools and add new ones to avoid duplicates
154
+ const nonMcpTools = prevTools.filter(
155
+ (tool) => !tool.code?.includes("mcpServerId:")
156
+ );
157
+ return [...nonMcpTools, ...mcpTools];
158
+ });
159
+ }, [getMCPToolsAsOriginalTools]);
160
 
161
  useEffect(() => {
162
  loadTools();
163
+ // Connect to MCP servers on startup
164
+ connectAllMCPServers().catch((error) => {
165
+ console.error("Failed to connect to MCP servers:", error);
166
+ });
167
+ }, [loadTools, connectAllMCPServers]);
168
 
169
  useEffect(() => {
170
  if (chatContainerRef.current) {
 
242
  const toggleToolCollapsed = (id: number): void => {
243
  setTools(
244
  tools.map((tool) =>
245
+ tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool
246
+ )
247
  );
248
  };
249
 
250
  const expandTool = (id: number): void => {
251
  setTools(
252
  tools.map((tool) =>
253
+ tool.id === id ? { ...tool, isCollapsed: false } : tool
254
+ )
255
  );
256
  };
257
 
 
278
  const toolToUse = tools.find((t) => t.name === name && t.enabled);
279
  if (!toolToUse) throw new Error(`Tool '${name}' not found or is disabled.`);
280
 
281
+ // Check if this is an MCP tool
282
+ const isMCPTool = toolToUse.code?.includes("mcpServerId:");
283
+ if (isMCPTool) {
284
+ // Extract MCP server ID and tool name from the code
285
+ const mcpServerMatch = toolToUse.code?.match(/mcpServerId: "([^"]+)"/);
286
+ const mcpToolMatch = toolToUse.code?.match(/toolName: "([^"]+)"/);
287
+
288
+ if (mcpServerMatch && mcpToolMatch) {
289
+ const serverId = mcpServerMatch[1];
290
+ const toolName = mcpToolMatch[1];
291
+
292
+ // Convert positional and keyword args to a single args object
293
+ const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
294
+ const schema = generateSchemaFromCode(functionCode);
295
+ const paramNames = Object.keys(schema.parameters.properties);
296
+
297
+ const args: Record<string, unknown> = {};
298
+
299
+ // Map positional args
300
+ for (
301
+ let i = 0;
302
+ i < Math.min(positionalArgs.length, paramNames.length);
303
+ i++
304
+ ) {
305
+ args[paramNames[i]] = positionalArgs[i];
306
+ }
307
+
308
+ // Map keyword args
309
+ Object.entries(keywordArgs).forEach(([key, value]) => {
310
+ args[key] = value;
311
+ });
312
+
313
+ // Call MCP tool
314
+ const mcpResult = await callMCPTool(serverId, toolName, args);
315
+ console.log("Raw MCP result:", mcpResult);
316
+
317
+ // Extract the actual content from MCP result
318
+ if (mcpResult.content && mcpResult.content.length > 0) {
319
+ // If there's text content, use that
320
+ const textContent = mcpResult.content.find((c) => c.text);
321
+ if (textContent && textContent.text) {
322
+ console.log("Using text content:", textContent.text);
323
+
324
+ // Try to handle double-encoded JSON
325
+ let content = textContent.text;
326
+ try {
327
+ // First, try to parse as JSON to see if it's a JSON string
328
+ const parsed = JSON.parse(content);
329
+ if (typeof parsed === "string") {
330
+ // If the parsed result is still a string, it was double-encoded
331
+ console.log("Detected double-encoded JSON, parsing again");
332
+ content = parsed;
333
+ } else {
334
+ // If it parsed to an object/array directly, use the original string
335
+ console.log("Using original text content");
336
+ }
337
+ } catch {
338
+ // If first parse fails, use original content
339
+ console.log("Using original text content (parse failed)");
340
+ }
341
+
342
+ return content;
343
+ }
344
+
345
+ // Otherwise, return the first content item's data or the full content array
346
+ const firstContent = mcpResult.content[0];
347
+ if (firstContent.data !== undefined) {
348
+ console.log("Using data content:", firstContent.data);
349
+ return JSON.stringify(firstContent.data);
350
+ }
351
+
352
+ // Fallback to the full content array
353
+ console.log("Using full content array:", mcpResult.content);
354
+ return JSON.stringify(mcpResult.content);
355
+ }
356
+
357
+ // If no content, return the full result
358
+ console.log("No content found, returning full result:", mcpResult);
359
+ return JSON.stringify(mcpResult);
360
+ }
361
+ }
362
+
363
+ // Handle local tools as before
364
  const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
365
  const schema = generateSchemaFromCode(functionCode);
366
  const paramNames = Object.keys(schema.parameters.properties);
367
 
368
+ const finalArgs: unknown[] = [];
369
  const requiredParams = schema.parameters.required || [];
370
 
371
  for (let i = 0; i < paramNames.length; ++i) {
372
  const paramName = paramNames[i];
373
  if (i < positionalArgs.length) {
374
  finalArgs.push(positionalArgs[i]);
375
+ } else if (Object.prototype.hasOwnProperty.call(keywordArgs, paramName)) {
376
  finalArgs.push(keywordArgs[paramName]);
377
  } else if (
378
+ Object.prototype.hasOwnProperty.call(
379
+ schema.parameters.properties[paramName],
380
+ "default"
381
+ )
382
  ) {
383
  finalArgs.push(schema.parameters.properties[paramName].default);
384
  } else if (!requiredParams.includes(paramName)) {
 
391
  const bodyMatch = functionCode.match(/function[^{]+\{([\s\S]*)\}/);
392
  if (!bodyMatch) {
393
  throw new Error(
394
+ "Could not parse function body. Ensure it's a standard `function` declaration."
395
  );
396
  }
397
  const body = bodyMatch[1];
398
  const AsyncFunction = Object.getPrototypeOf(
399
+ async function () {}
400
  ).constructor;
401
  const func = new AsyncFunction(...paramNames, body);
402
  const result = await func(...finalArgs);
 
404
  };
405
 
406
  const executeToolCalls = async (
407
+ toolCallContent: string
408
  ): Promise<RenderInfo[]> => {
409
  const toolCalls = extractPythonicCalls(toolCallContent);
410
  if (toolCalls.length === 0)
 
422
  ? extractFunctionAndRenderer(toolUsed.code)
423
  : { rendererCode: undefined };
424
 
425
+ console.log("Tool renderer info:", {
426
+ toolName: toolUsed?.name,
427
+ hasRenderer: !!rendererCode,
428
+ rendererCode,
429
+ });
430
+
431
  let parsedResult;
432
  try {
433
  parsedResult = JSON.parse(result);
434
+ console.log("Parsed result in executeToolCalls:", parsedResult);
435
  } catch {
436
  parsedResult = result;
437
+ console.log("Failed to parse result, using as string:", result);
438
  }
439
 
440
+ let namedParams: Record<string, unknown> = Object.create(null);
441
  if (parsedCall && toolUsed) {
442
  const schema = generateSchemaFromCode(
443
+ extractFunctionAndRenderer(toolUsed.code).functionCode
444
  );
445
  const paramNames = Object.keys(schema.parameters.properties);
446
  namedParams = mapArgsToNamedParams(
447
  paramNames,
448
  parsedCall.positionalArgs,
449
+ parsedCall.keywordArgs
450
  );
451
  }
452
 
 
456
  renderer: rendererCode,
457
  input: namedParams,
458
  });
459
+
460
+ console.log("RenderInfo created:", {
461
+ call,
462
+ result: parsedResult,
463
+ renderer: rendererCode,
464
+ hasRenderer: !!rendererCode,
465
+ resultType: typeof parsedResult,
466
+ isArray: Array.isArray(parsedResult),
467
+ });
468
  } catch (error) {
469
  const errorMessage = getErrorMessage(error);
470
  results.push({ call, error: errorMessage });
 
477
  if (!input.trim() || !isReady) return;
478
 
479
  const userMessage: Message = { role: "user", content: input };
480
+ const currentMessages: Message[] = [...messages, userMessage];
481
  setMessages(currentMessages);
482
  setInput("");
483
  setIsGenerating(true);
 
509
  };
510
  return updated;
511
  });
512
+ }
513
  );
514
 
515
  currentMessages.push({ role: "assistant", content: response });
 
568
  console.error("Failed to save system prompt:", error);
569
  }
570
  },
571
+ []
572
  );
573
 
574
  const loadSelectedModel = useCallback(async (): Promise<void> => {
 
577
  } catch (error) {
578
  console.error("Failed to load model:", error);
579
  }
580
+ }, [loadModel]);
581
 
582
  const loadSelectedModelId = useCallback(async (): Promise<void> => {
583
  try {
 
627
  console.error("Failed to save selected model ID:", error);
628
  }
629
  },
630
+ []
631
  );
632
 
633
  useEffect(() => {
 
678
  };
679
  return updated;
680
  });
681
+ }
682
  );
683
 
684
  currentMessages.push({ role: "assistant", content: response });
 
730
  />
731
  ) : (
732
  <div className="flex h-screen text-white">
733
+ <div
734
+ className={`flex flex-col p-4 transition-all duration-300 ${
735
+ isToolsPanelVisible ? "w-1/2" : "w-full"
736
+ }`}
737
+ >
738
  <div className="flex items-center justify-between mb-4">
739
  <div className="flex items-center gap-3">
740
  <h1 className="text-3xl font-bold text-gray-200">
 
765
  >
766
  <Settings size={16} />
767
  </button>
768
+ <button
769
+ onClick={() => setIsMCPManagerOpen(true)}
770
+ className="h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors bg-blue-600 hover:bg-blue-700 text-sm"
771
+ title="Manage MCP Servers"
772
+ >
773
+ 🌐
774
+ </button>
775
+ <button
776
+ onClick={() => setIsToolsPanelVisible(!isToolsPanelVisible)}
777
+ className="h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors bg-gray-600 hover:bg-gray-700 text-sm"
778
+ title={
779
+ isToolsPanelVisible
780
+ ? "Hide Tools Panel"
781
+ : "Show Tools Panel"
782
+ }
783
+ >
784
+ {isToolsPanelVisible ? (
785
+ <PanelRightClose size={16} />
786
+ ) : (
787
+ <PanelRightOpen size={16} />
788
+ )}
789
+ </button>
790
  </div>
791
  </div>
792
 
 
812
  );
813
  } else if (msg.role === "assistant") {
814
  const isToolCall = msg.content.includes(
815
+ "<|tool_call_start|>"
816
  );
817
 
818
  if (isToolCall) {
 
821
  const hasError =
822
  isCompleted &&
823
  (nextMessage as ToolMessage).renderInfo.some(
824
+ (info) => !!info.error
825
  );
826
 
827
  return (
 
849
  } else if (msg.role === "tool") {
850
  const visibleToolResults = msg.renderInfo.filter(
851
  (info) =>
852
+ info.error || (info.result != null && info.renderer)
853
  );
854
 
855
  if (visibleToolResults.length === 0) return null;
 
867
  <ResultBlock error={info.error} />
868
  ) : (
869
  <ToolResultRenderer
870
+ result={
871
+ info.result as import("./types/mcp").MCPToolResult
872
+ }
873
  rendererCode={info.renderer}
874
  input={info.input}
875
  />
 
916
  </div>
917
  </div>
918
 
919
+ {isToolsPanelVisible && (
920
+ <div className="w-1/2 flex flex-col p-4 border-l border-gray-700 transition-all duration-300">
921
+ <div className="flex justify-between items-center mb-4">
922
+ <h2 className="text-2xl font-bold text-teal-400">Tools</h2>
923
+ <button
924
+ onClick={addTool}
925
+ className="flex items-center bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
926
+ >
927
+ <Plus size={16} className="mr-2" /> Add Tool
928
+ </button>
929
+ </div>
930
+ <div
931
+ ref={toolsContainerRef}
932
+ className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto space-y-3"
933
  >
934
+ {tools.map((tool) => (
935
+ <ToolItem
936
+ key={tool.id}
937
+ tool={tool}
938
+ onToggleEnabled={() => toggleToolEnabled(tool.id)}
939
+ onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
940
+ onExpand={() => expandTool(tool.id)}
941
+ onDelete={() => deleteTool(tool.id)}
942
+ onCodeChange={(newCode) =>
943
+ handleToolCodeChange(tool.id, newCode)
944
+ }
945
+ />
946
+ ))}
947
+ </div>
 
 
 
 
 
 
948
  </div>
949
+ )}
950
  </div>
951
  )}
952
 
 
992
  </div>
993
  </div>
994
  )}
995
+
996
+ {/* MCP Server Manager Modal */}
997
+ <MCPServerManager
998
+ isOpen={isMCPManagerOpen}
999
+ onClose={() => setIsMCPManagerOpen(false)}
1000
+ />
1001
  </div>
1002
  );
1003
  };
src/components/ResultBlock.tsx CHANGED
@@ -1,21 +1,144 @@
1
  import type React from "react";
 
2
 
3
- const ResultBlock: React.FC<{ error?: string; result?: any }> = ({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  error,
5
  result,
6
- }) => (
7
- <div
8
- className={
9
- error
10
- ? "bg-red-900 border border-red-600 rounded p-3"
11
- : "bg-gray-700 border border-gray-600 rounded p-3"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
- >
14
- {error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
15
- <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto mt-2">
16
- {typeof result === "object" ? JSON.stringify(result, null, 2) : result}
17
- </pre>
18
- </div>
19
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  export default ResultBlock;
 
1
  import type React from "react";
2
+ import { useState } from "react";
3
 
4
+ type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
5
+ type JsonObject = { [key: string]: JsonValue };
6
+ type JsonArray = JsonValue[];
7
+
8
+ const isJsonString = (str: string): boolean => {
9
+ if (typeof str !== "string") return false;
10
+ const trimmed = str.trim();
11
+
12
+ // Check if it looks like JSON (starts with { or [)
13
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false;
14
+
15
+ try {
16
+ const parsed = JSON.parse(trimmed);
17
+ // Ensure it's actually an object or array, not just a primitive
18
+ console.log("JSON parse successful:", parsed);
19
+ return typeof parsed === "object" && parsed !== null;
20
+ } catch (error) {
21
+ console.log("JSON parse failed:", error);
22
+ return false;
23
+ }
24
+ };
25
+
26
+ const JsonCollapsible: React.FC<{ data: JsonValue; isString?: boolean }> = ({
27
+ data,
28
+ isString = false,
29
+ }) => {
30
+ const [isCollapsed, setIsCollapsed] = useState(true);
31
+
32
+ let jsonData: JsonValue;
33
+ try {
34
+ jsonData = isString ? JSON.parse(data as string) : data;
35
+ } catch {
36
+ // If parsing fails, display as string
37
+ return (
38
+ <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto">
39
+ {String(data)}
40
+ </pre>
41
+ );
42
+ }
43
+
44
+ const isObject = typeof jsonData === "object" && jsonData !== null;
45
+
46
+ if (!isObject) {
47
+ return <span className="text-gray-300">{JSON.stringify(jsonData)}</span>;
48
+ }
49
+
50
+ const keys = Object.keys(jsonData as JsonObject);
51
+ const preview = Array.isArray(jsonData)
52
+ ? `[${jsonData.length} items]`
53
+ : `{${keys.length} keys}`;
54
+
55
+ return (
56
+ <div className="json-collapsible">
57
+ <button
58
+ onClick={() => setIsCollapsed(!isCollapsed)}
59
+ className="flex items-center gap-1 text-blue-400 hover:text-blue-300 transition-colors text-sm font-mono"
60
+ >
61
+ <span
62
+ className="transform transition-transform duration-200"
63
+ style={{
64
+ transform: isCollapsed ? "rotate(-90deg)" : "rotate(0deg)",
65
+ }}
66
+ >
67
+
68
+ </span>
69
+ {isCollapsed ? preview : Array.isArray(jsonData) ? "[" : "{"}
70
+ </button>
71
+ {!isCollapsed && (
72
+ <div className="ml-4 mt-1">
73
+ <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto">
74
+ {JSON.stringify(jsonData, null, 2)}
75
+ </pre>
76
+ <div className="text-blue-400 font-mono text-sm">
77
+ {Array.isArray(jsonData) ? "]" : "}"}
78
+ </div>
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ };
84
+
85
+ const ResultBlock: React.FC<{ error?: string; result?: unknown }> = ({
86
  error,
87
  result,
88
+ }) => {
89
+ console.log("ResultBlock component rendered with:", {
90
+ error,
91
+ result,
92
+ type: typeof result,
93
+ });
94
+
95
+ const renderContent = () => {
96
+ console.log("ResultBlock Debug:", {
97
+ result,
98
+ type: typeof result,
99
+ isString: typeof result === "string",
100
+ isArray: Array.isArray(result),
101
+ startsWithBracket:
102
+ typeof result === "string" && result.trim().startsWith("["),
103
+ isJsonString: typeof result === "string" ? isJsonString(result) : false,
104
+ });
105
+
106
+ // Handle objects and arrays directly
107
+ if (typeof result === "object" && result !== null) {
108
+ console.log("Rendering as object/array with JsonCollapsible");
109
+ return <JsonCollapsible data={result as JsonValue} />;
110
  }
111
+
112
+ // Handle string that might be JSON
113
+ if (typeof result === "string") {
114
+ const trimmed = result.trim();
115
+ if (isJsonString(trimmed)) {
116
+ console.log("Rendering string as JSON with JsonCollapsible");
117
+ return <JsonCollapsible data={trimmed} isString={true} />;
118
+ }
119
+ }
120
+
121
+ // Fallback to plain text display
122
+ console.log("Rendering as plain text fallback");
123
+ return (
124
+ <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto">
125
+ {String(result)}
126
+ </pre>
127
+ );
128
+ };
129
+
130
+ return (
131
+ <div
132
+ className={
133
+ error
134
+ ? "bg-red-900 border border-red-600 rounded p-3"
135
+ : "bg-gray-700 border border-gray-600 rounded p-3"
136
+ }
137
+ >
138
+ {error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
139
+ <div className="mt-2">{renderContent()}</div>
140
+ </div>
141
+ );
142
+ };
143
 
144
  export default ResultBlock;
src/components/ToolResultRenderer.tsx CHANGED
@@ -1,10 +1,11 @@
1
  import React from "react";
2
  import ResultBlock from "./ResultBlock";
 
3
 
4
  const ToolResultRenderer: React.FC<{
5
- result: any;
6
  rendererCode?: string;
7
- input?: any;
8
  }> = ({ result, rendererCode, input }) => {
9
  if (!rendererCode) {
10
  return <ResultBlock result={result} />;
@@ -25,10 +26,34 @@ const ToolResultRenderer: React.FC<{
25
  const { createElement: h, Fragment } = React;
26
  const JSXComponent = ${componentCode};
27
  return JSXComponent(input, output);
28
- `,
29
  );
30
 
31
  const element = componentFunction(React, input || {}, result);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  return element;
33
  } catch (error) {
34
  return (
 
1
  import React from "react";
2
  import ResultBlock from "./ResultBlock";
3
+ import type { MCPToolResult } from "../types/mcp";
4
 
5
  const ToolResultRenderer: React.FC<{
6
+ result: MCPToolResult;
7
  rendererCode?: string;
8
+ input?: unknown;
9
  }> = ({ result, rendererCode, input }) => {
10
  if (!rendererCode) {
11
  return <ResultBlock result={result} />;
 
26
  const { createElement: h, Fragment } = React;
27
  const JSXComponent = ${componentCode};
28
  return JSXComponent(input, output);
29
+ `
30
  );
31
 
32
  const element = componentFunction(React, input || {}, result);
33
+
34
+ // For object results, also show the formatted JSON below the custom renderer
35
+ if (typeof result === "object" && result !== null) {
36
+ return (
37
+ <div>
38
+ {element}
39
+ <div className="mt-3 space-y-3">
40
+ <div>
41
+ <h4 className="text-sm font-medium text-gray-300 mb-2">
42
+ Request
43
+ </h4>
44
+ <ResultBlock result={input || {}} />
45
+ </div>
46
+ <div>
47
+ <h4 className="text-sm font-medium text-gray-300 mb-2">
48
+ Response
49
+ </h4>
50
+ <ResultBlock result={result} />
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
  return element;
58
  } catch (error) {
59
  return (
src/constants/models.ts CHANGED
@@ -2,4 +2,9 @@ export const MODEL_OPTIONS = [
2
  { id: "350M", label: "LFM2-350M", size: "350M parameters (312 MB)" },
3
  { id: "700M", label: "LFM2-700M", size: "700M parameters (579 MB)" },
4
  { id: "1.2B", label: "LFM2-1.2B", size: "1.2B parameters (868 MB)" },
 
 
 
 
 
5
  ];
 
2
  { id: "350M", label: "LFM2-350M", size: "350M parameters (312 MB)" },
3
  { id: "700M", label: "LFM2-700M", size: "700M parameters (579 MB)" },
4
  { id: "1.2B", label: "LFM2-1.2B", size: "1.2B parameters (868 MB)" },
5
+ {
6
+ id: "gemma-270m",
7
+ label: "Gemma-3-270M",
8
+ size: "270M parameters",
9
+ },
10
  ];
src/hooks/useLLM.ts CHANGED
@@ -13,11 +13,11 @@ interface LLMState {
13
  }
14
 
15
  interface LLMInstance {
16
- model: any;
17
- tokenizer: any;
18
  }
19
 
20
- let moduleCache: {
21
  [modelId: string]: {
22
  instance: LLMInstance | null;
23
  loadingPromise: Promise<LLMInstance> | null;
@@ -36,14 +36,20 @@ export const useLLM = (modelId?: string) => {
36
  const loadingPromiseRef = useRef<Promise<LLMInstance> | null>(null);
37
 
38
  const abortControllerRef = useRef<AbortController | null>(null);
39
- const pastKeyValuesRef = useRef<any>(null);
40
 
41
  const loadModel = useCallback(async () => {
42
  if (!modelId) {
43
  throw new Error("Model ID is required");
44
  }
45
 
46
- const MODEL_ID = `onnx-community/LFM2-${modelId}-ONNX`;
 
 
 
 
 
 
47
 
48
  if (!moduleCache[modelId]) {
49
  moduleCache[modelId] = {
@@ -99,7 +105,7 @@ export const useLLM = (modelId?: string) => {
99
  progress.file.endsWith(".onnx_data")
100
  ) {
101
  const percentage = Math.round(
102
- (progress.loaded / progress.total) * 100,
103
  );
104
  setState((prev) => ({ ...prev, progress: percentage }));
105
  }
@@ -150,7 +156,7 @@ export const useLLM = (modelId?: string) => {
150
  async (
151
  messages: Array<{ role: string; content: string }>,
152
  tools: Array<any>,
153
- onToken?: (token: string) => void,
154
  ): Promise<string> => {
155
  const instance = instanceRef.current;
156
  if (!instance) {
@@ -196,7 +202,7 @@ export const useLLM = (modelId?: string) => {
196
 
197
  return response;
198
  },
199
- [],
200
  );
201
 
202
  const clearPastKeyValues = useCallback(() => {
 
13
  }
14
 
15
  interface LLMInstance {
16
+ model: unknown;
17
+ tokenizer: unknown;
18
  }
19
 
20
+ const moduleCache: {
21
  [modelId: string]: {
22
  instance: LLMInstance | null;
23
  loadingPromise: Promise<LLMInstance> | null;
 
36
  const loadingPromiseRef = useRef<Promise<LLMInstance> | null>(null);
37
 
38
  const abortControllerRef = useRef<AbortController | null>(null);
39
+ const pastKeyValuesRef = useRef<unknown>(null);
40
 
41
  const loadModel = useCallback(async () => {
42
  if (!modelId) {
43
  throw new Error("Model ID is required");
44
  }
45
 
46
+ // Handle different model types
47
+ let MODEL_ID: string;
48
+ if (modelId === "gemma-270m") {
49
+ MODEL_ID = "onnx-community/gemma-3-270m-it-ONNX";
50
+ } else {
51
+ MODEL_ID = `onnx-community/LFM2-${modelId}-ONNX`;
52
+ }
53
 
54
  if (!moduleCache[modelId]) {
55
  moduleCache[modelId] = {
 
105
  progress.file.endsWith(".onnx_data")
106
  ) {
107
  const percentage = Math.round(
108
+ (progress.loaded / progress.total) * 100
109
  );
110
  setState((prev) => ({ ...prev, progress: percentage }));
111
  }
 
156
  async (
157
  messages: Array<{ role: string; content: string }>,
158
  tools: Array<any>,
159
+ onToken?: (token: string) => void
160
  ): Promise<string> => {
161
  const instance = instanceRef.current;
162
  if (!instance) {
 
202
 
203
  return response;
204
  },
205
+ []
206
  );
207
 
208
  const clearPastKeyValues = useCallback(() => {
vite.config.ts CHANGED
@@ -5,4 +5,14 @@ import tailwindcss from "@tailwindcss/vite";
5
  // https://vite.dev/config/
6
  export default defineConfig({
7
  plugins: [react(), tailwindcss()],
 
 
 
 
 
 
 
 
 
 
8
  });
 
5
  // https://vite.dev/config/
6
  export default defineConfig({
7
  plugins: [react(), tailwindcss()],
8
+ server: {
9
+ proxy: {
10
+ "/api/mcp": {
11
+ target: "https://api.shreyas.natoma.dev",
12
+ changeOrigin: true,
13
+ secure: true,
14
+ rewrite: (path) => path.replace(/^\/api\/mcp/, "/mcp"),
15
+ },
16
+ },
17
+ },
18
  });