shreyask commited on
Commit
8c21d03
·
verified ·
1 Parent(s): ebde7f4

feat: add conversation management functionality and update MCPClientService to handle resources and prompts

Browse files
package.json CHANGED
@@ -11,7 +11,7 @@
11
  },
12
  "dependencies": {
13
  "@huggingface/transformers": "^3.7.5",
14
- "@modelcontextprotocol/sdk": "^1.17.3",
15
  "@monaco-editor/react": "^4.7.0",
16
  "@tailwindcss/vite": "^4.1.11",
17
  "dompurify": "^3.2.7",
 
11
  },
12
  "dependencies": {
13
  "@huggingface/transformers": "^3.7.5",
14
+ "@modelcontextprotocol/sdk": "^1.25.1",
15
  "@monaco-editor/react": "^4.7.0",
16
  "@tailwindcss/vite": "^4.1.11",
17
  "dompurify": "^3.2.7",
src/App.tsx CHANGED
@@ -35,7 +35,8 @@ import {
35
  } from "./utils";
36
 
37
  import { DEFAULT_SYSTEM_PROMPT } from "./constants/systemPrompt";
38
- import { DB_NAME, STORE_NAME, SETTINGS_STORE_NAME } from "./constants/db";
 
39
 
40
  import { TEMPLATE } from "./tools";
41
  import ToolResultRenderer from "./components/ToolResultRenderer";
@@ -66,22 +67,81 @@ interface ToolMessage {
66
  }
67
  type Message = BaseMessage | ToolMessage;
68
 
 
 
 
 
 
 
 
 
69
  async function getDB(): Promise<IDBPDatabase> {
70
- return openDB(DB_NAME, 1, {
71
- upgrade(db) {
72
- if (!db.objectStoreNames.contains(STORE_NAME)) {
73
- db.createObjectStore(STORE_NAME, {
74
- keyPath: "id",
75
- autoIncrement: true,
76
- });
 
 
 
 
 
77
  }
78
- if (!db.objectStoreNames.contains(SETTINGS_STORE_NAME)) {
79
- db.createObjectStore(SETTINGS_STORE_NAME, { keyPath: "key" });
 
 
 
 
 
 
80
  }
81
  },
82
  });
83
  }
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  function renderMarkdown(text: string): string {
86
  return DOMPurify.sanitize(marked.parse(text) as string);
87
  }
@@ -105,6 +165,9 @@ const App: React.FC = () => {
105
  useState<boolean>(false);
106
  const [isMCPManagerOpen, setIsMCPManagerOpen] = useState<boolean>(false);
107
  const [isToolsPanelVisible, setIsToolsPanelVisible] = useState<boolean>(false);
 
 
 
108
  const chatContainerRef = useRef<HTMLDivElement>(null);
109
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
110
  const toolsContainerRef = useRef<HTMLDivElement>(null);
@@ -207,6 +270,42 @@ const App: React.FC = () => {
207
  }
208
  }, [messages]);
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  const updateToolInDB = async (tool: Tool): Promise<void> => {
211
  const db = await getDB();
212
  await db.put(STORE_NAME, tool);
@@ -228,6 +327,34 @@ const App: React.FC = () => {
228
  clearPastKeyValues();
229
  }, [clearPastKeyValues]);
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  const addTool = async (): Promise<void> => {
232
  const newTool: Omit<Tool, "id"> = {
233
  name: "new_tool",
@@ -698,9 +825,52 @@ const App: React.FC = () => {
698
  />
699
  ) : (
700
  <div className="flex h-screen text-white">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  <div
702
  className={`flex flex-col p-4 transition-all duration-300 ${
703
- isToolsPanelVisible ? "w-1/2" : "w-full"
704
  }`}
705
  >
706
  <div className="flex items-center justify-between mb-4">
@@ -712,17 +882,28 @@ const App: React.FC = () => {
712
  <Zap size={16} className="mr-2" />
713
  Ready
714
  </div>
 
 
 
 
 
 
 
 
 
 
 
715
  <button
716
  disabled={isGenerating}
717
- onClick={clearChat}
718
  className={`h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors text-sm ${
719
  isGenerating
720
  ? "bg-gray-600 cursor-not-allowed opacity-50"
721
  : "bg-gray-600 hover:bg-gray-700"
722
  }`}
723
- title="Clear chat"
724
  >
725
- <RotateCcw size={14} className="mr-2" /> Clear
726
  </button>
727
  <button
728
  onClick={handleOpenSystemPromptModal}
 
35
  } from "./utils";
36
 
37
  import { DEFAULT_SYSTEM_PROMPT } from "./constants/systemPrompt";
38
+
39
+ import { DB_NAME, STORE_NAME, SETTINGS_STORE_NAME, CONVERSATIONS_STORE_NAME } from "./constants/db";
40
 
41
  import { TEMPLATE } from "./tools";
42
  import ToolResultRenderer from "./components/ToolResultRenderer";
 
67
  }
68
  type Message = BaseMessage | ToolMessage;
69
 
70
+ interface Conversation {
71
+ id?: number;
72
+ title: string;
73
+ messages: Message[];
74
+ createdAt: number;
75
+ updatedAt: number;
76
+ }
77
+
78
  async function getDB(): Promise<IDBPDatabase> {
79
+ return openDB(DB_NAME, 2, {
80
+ upgrade(db, oldVersion) {
81
+ if (oldVersion < 1) {
82
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
83
+ db.createObjectStore(STORE_NAME, {
84
+ keyPath: "id",
85
+ autoIncrement: true,
86
+ });
87
+ }
88
+ if (!db.objectStoreNames.contains(SETTINGS_STORE_NAME)) {
89
+ db.createObjectStore(SETTINGS_STORE_NAME, { keyPath: "key" });
90
+ }
91
  }
92
+ if (oldVersion < 2) {
93
+ if (!db.objectStoreNames.contains(CONVERSATIONS_STORE_NAME)) {
94
+ const conversationStore = db.createObjectStore(CONVERSATIONS_STORE_NAME, {
95
+ keyPath: "id",
96
+ autoIncrement: true,
97
+ });
98
+ conversationStore.createIndex("updatedAt", "updatedAt");
99
+ }
100
  }
101
  },
102
  });
103
  }
104
 
105
+ // Conversation management functions
106
+ async function saveConversation(conversation: Conversation): Promise<number> {
107
+ const db = await getDB();
108
+ const id = await db.put(CONVERSATIONS_STORE_NAME, {
109
+ ...conversation,
110
+ updatedAt: Date.now(),
111
+ });
112
+ return id as number;
113
+ }
114
+
115
+ async function loadConversation(id: number): Promise<Conversation | undefined> {
116
+ const db = await getDB();
117
+ return db.get(CONVERSATIONS_STORE_NAME, id);
118
+ }
119
+
120
+ async function loadAllConversations(): Promise<Conversation[]> {
121
+ const db = await getDB();
122
+ const conversations = await db.getAllFromIndex(
123
+ CONVERSATIONS_STORE_NAME,
124
+ "updatedAt"
125
+ );
126
+ return conversations.reverse(); // Most recent first
127
+ }
128
+
129
+ async function deleteConversation(id: number): Promise<void> {
130
+ const db = await getDB();
131
+ await db.delete(CONVERSATIONS_STORE_NAME, id);
132
+ }
133
+
134
+ function generateConversationTitle(messages: Message[]): string {
135
+ const firstUserMessage = messages.find((m) => m.role === "user");
136
+ if (firstUserMessage) {
137
+ const content = firstUserMessage.content.substring(0, 50);
138
+ return content.length < firstUserMessage.content.length
139
+ ? content + "..."
140
+ : content;
141
+ }
142
+ return "New Conversation";
143
+ }
144
+
145
  function renderMarkdown(text: string): string {
146
  return DOMPurify.sanitize(marked.parse(text) as string);
147
  }
 
165
  useState<boolean>(false);
166
  const [isMCPManagerOpen, setIsMCPManagerOpen] = useState<boolean>(false);
167
  const [isToolsPanelVisible, setIsToolsPanelVisible] = useState<boolean>(false);
168
+ const [currentConversationId, setCurrentConversationId] = useState<number | null>(null);
169
+ const [conversations, setConversations] = useState<Conversation[]>([]);
170
+ const [isConversationsPanelVisible, setIsConversationsPanelVisible] = useState<boolean>(false);
171
  const chatContainerRef = useRef<HTMLDivElement>(null);
172
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
173
  const toolsContainerRef = useRef<HTMLDivElement>(null);
 
270
  }
271
  }, [messages]);
272
 
273
+ // Load all conversations on mount
274
+ useEffect(() => {
275
+ loadAllConversations().then(setConversations).catch((error) => {
276
+ console.error("Failed to load conversations:", error);
277
+ });
278
+ }, []);
279
+
280
+ // Auto-save current conversation when messages change
281
+ useEffect(() => {
282
+ if (messages.length === 0) return;
283
+
284
+ const saveCurrentConversation = async () => {
285
+ const title = generateConversationTitle(messages);
286
+ const conversation: Conversation = {
287
+ ...(currentConversationId ? { id: currentConversationId } : {}),
288
+ title,
289
+ messages,
290
+ createdAt: currentConversationId ? conversations.find(c => c.id === currentConversationId)?.createdAt || Date.now() : Date.now(),
291
+ updatedAt: Date.now(),
292
+ };
293
+
294
+ const id = await saveConversation(conversation);
295
+ if (!currentConversationId) {
296
+ setCurrentConversationId(id);
297
+ }
298
+
299
+ // Reload conversations list
300
+ const updatedConversations = await loadAllConversations();
301
+ setConversations(updatedConversations);
302
+ };
303
+
304
+ saveCurrentConversation().catch((error) => {
305
+ console.error("Failed to save conversation:", error);
306
+ });
307
+ }, [messages, currentConversationId, conversations]);
308
+
309
  const updateToolInDB = async (tool: Tool): Promise<void> => {
310
  const db = await getDB();
311
  await db.put(STORE_NAME, tool);
 
327
  clearPastKeyValues();
328
  }, [clearPastKeyValues]);
329
 
330
+ // Conversation management handlers
331
+ const handleNewConversation = useCallback(() => {
332
+ setMessages([]);
333
+ setCurrentConversationId(null);
334
+ clearPastKeyValues();
335
+ }, [clearPastKeyValues]);
336
+
337
+ const handleLoadConversation = useCallback(async (id: number) => {
338
+ const conversation = await loadConversation(id);
339
+ if (conversation) {
340
+ setMessages(conversation.messages);
341
+ setCurrentConversationId(id);
342
+ clearPastKeyValues();
343
+ setIsConversationsPanelVisible(false);
344
+ }
345
+ }, [clearPastKeyValues]);
346
+
347
+ const handleDeleteConversation = useCallback(async (id: number) => {
348
+ await deleteConversation(id);
349
+ const updatedConversations = await loadAllConversations();
350
+ setConversations(updatedConversations);
351
+
352
+ // If deleting current conversation, start a new one
353
+ if (id === currentConversationId) {
354
+ handleNewConversation();
355
+ }
356
+ }, [currentConversationId, handleNewConversation]);
357
+
358
  const addTool = async (): Promise<void> => {
359
  const newTool: Omit<Tool, "id"> = {
360
  name: "new_tool",
 
825
  />
826
  ) : (
827
  <div className="flex h-screen text-white">
828
+ {isConversationsPanelVisible && (
829
+ <div className="w-80 flex flex-col p-4 border-r border-gray-700 bg-gray-900">
830
+ <h2 className="text-2xl font-bold text-indigo-400 mb-4">Conversations</h2>
831
+ <div className="flex-grow overflow-y-auto space-y-2">
832
+ {conversations.length === 0 ? (
833
+ <p className="text-gray-400 text-sm text-center mt-8">
834
+ No saved conversations yet
835
+ </p>
836
+ ) : (
837
+ conversations.map((conv) => (
838
+ <div
839
+ key={conv.id}
840
+ className={`p-3 rounded-lg cursor-pointer transition-colors ${
841
+ conv.id === currentConversationId
842
+ ? "bg-indigo-700"
843
+ : "bg-gray-800 hover:bg-gray-700"
844
+ }`}
845
+ onClick={() => handleLoadConversation(conv.id!)}
846
+ >
847
+ <div className="flex justify-between items-start mb-1">
848
+ <h3 className="text-sm font-semibold text-white truncate flex-1">
849
+ {conv.title}
850
+ </h3>
851
+ <button
852
+ onClick={(e) => {
853
+ e.stopPropagation();
854
+ handleDeleteConversation(conv.id!);
855
+ }}
856
+ className="text-gray-400 hover:text-red-400 ml-2"
857
+ title="Delete conversation"
858
+ >
859
+ <X size={14} />
860
+ </button>
861
+ </div>
862
+ <p className="text-xs text-gray-400">
863
+ {new Date(conv.updatedAt).toLocaleDateString()}
864
+ </p>
865
+ </div>
866
+ ))
867
+ )}
868
+ </div>
869
+ </div>
870
+ )}
871
  <div
872
  className={`flex flex-col p-4 transition-all duration-300 ${
873
+ isToolsPanelVisible ? "w-1/2" : isConversationsPanelVisible ? "flex-1" : "w-full"
874
  }`}
875
  >
876
  <div className="flex items-center justify-between mb-4">
 
882
  <Zap size={16} className="mr-2" />
883
  Ready
884
  </div>
885
+ <button
886
+ onClick={() => setIsConversationsPanelVisible(!isConversationsPanelVisible)}
887
+ 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"
888
+ title={
889
+ isConversationsPanelVisible
890
+ ? "Hide Conversations"
891
+ : "Show Conversations"
892
+ }
893
+ >
894
+ 📝
895
+ </button>
896
  <button
897
  disabled={isGenerating}
898
+ onClick={handleNewConversation}
899
  className={`h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors text-sm ${
900
  isGenerating
901
  ? "bg-gray-600 cursor-not-allowed opacity-50"
902
  : "bg-gray-600 hover:bg-gray-700"
903
  }`}
904
+ title="New conversation"
905
  >
906
+ <Plus size={14} className="mr-2" /> New
907
  </button>
908
  <button
909
  onClick={handleOpenSystemPromptModal}
src/constants/db.ts CHANGED
@@ -1,3 +1,4 @@
1
  export const DB_NAME = "tool-caller-db";
2
  export const STORE_NAME = "tools";
3
  export const SETTINGS_STORE_NAME = "settings";
 
 
1
  export const DB_NAME = "tool-caller-db";
2
  export const STORE_NAME = "tools";
3
  export const SETTINGS_STORE_NAME = "settings";
4
+ export const CONVERSATIONS_STORE_NAME = "conversations";
src/services/mcpClient.ts CHANGED
@@ -72,6 +72,8 @@ export class MCPClientService {
72
  config,
73
  isConnected: false,
74
  tools: [],
 
 
75
  lastError: undefined,
76
  lastConnected: undefined,
77
  };
@@ -107,6 +109,8 @@ export class MCPClientService {
107
  config,
108
  isConnected: false,
109
  tools: [],
 
 
110
  lastError: undefined,
111
  lastConnected: undefined,
112
  };
@@ -155,6 +159,8 @@ export class MCPClientService {
155
  {
156
  capabilities: {
157
  tools: {},
 
 
158
  },
159
  }
160
  );
@@ -226,12 +232,34 @@ export class MCPClientService {
226
  // Connect to the server
227
  await client.connect(transport);
228
 
229
- // List available tools
230
  const toolsResult = await client.listTools();
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  // Update connection state
233
  connection.isConnected = true;
234
  connection.tools = toolsResult.tools;
 
 
235
  connection.lastError = undefined;
236
  connection.lastConnected = new Date();
237
 
@@ -283,6 +311,32 @@ export class MCPClientService {
283
  return allTools;
284
  }
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  // Call a tool on an MCP server
287
  async callTool(
288
  serverId: string,
 
72
  config,
73
  isConnected: false,
74
  tools: [],
75
+ resources: [],
76
+ prompts: [],
77
  lastError: undefined,
78
  lastConnected: undefined,
79
  };
 
109
  config,
110
  isConnected: false,
111
  tools: [],
112
+ resources: [],
113
+ prompts: [],
114
  lastError: undefined,
115
  lastConnected: undefined,
116
  };
 
159
  {
160
  capabilities: {
161
  tools: {},
162
+ resources: {},
163
+ prompts: {},
164
  },
165
  }
166
  );
 
232
  // Connect to the server
233
  await client.connect(transport);
234
 
235
+ // List available tools (always required)
236
  const toolsResult = await client.listTools();
237
 
238
+ // List resources if supported (optional - not all servers implement this)
239
+ let resources: any[] = [];
240
+ try {
241
+ const resourcesResult = await client.listResources();
242
+ resources = resourcesResult.resources;
243
+ } catch (error) {
244
+ // Server doesn't support resources - that's okay
245
+ console.log(`Server ${serverId} does not support resources:`, error instanceof Error ? error.message : String(error));
246
+ }
247
+
248
+ // List prompts if supported (optional - not all servers implement this)
249
+ let prompts: any[] = [];
250
+ try {
251
+ const promptsResult = await client.listPrompts();
252
+ prompts = promptsResult.prompts;
253
+ } catch (error) {
254
+ // Server doesn't support prompts - that's okay
255
+ console.log(`Server ${serverId} does not support prompts:`, error instanceof Error ? error.message : String(error));
256
+ }
257
+
258
  // Update connection state
259
  connection.isConnected = true;
260
  connection.tools = toolsResult.tools;
261
+ connection.resources = resources;
262
+ connection.prompts = prompts;
263
  connection.lastError = undefined;
264
  connection.lastConnected = new Date();
265
 
 
311
  return allTools;
312
  }
313
 
314
+ // Get all resources from all connected servers
315
+ getAllResources() {
316
+ const allResources: any[] = [];
317
+
318
+ for (const connection of this.connections.values()) {
319
+ if (connection.isConnected && connection.config.enabled) {
320
+ allResources.push(...connection.resources);
321
+ }
322
+ }
323
+
324
+ return allResources;
325
+ }
326
+
327
+ // Get all prompts from all connected servers
328
+ getAllPrompts() {
329
+ const allPrompts: any[] = [];
330
+
331
+ for (const connection of this.connections.values()) {
332
+ if (connection.isConnected && connection.config.enabled) {
333
+ allPrompts.push(...connection.prompts);
334
+ }
335
+ }
336
+
337
+ return allPrompts;
338
+ }
339
+
340
  // Call a tool on an MCP server
341
  async callTool(
342
  serverId: string,
src/types/mcp.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js";
 
 
 
 
2
 
3
  export interface MCPServerConfig {
4
  id: string;
@@ -18,6 +22,8 @@ export interface MCPServerConnection {
18
  config: MCPServerConfig;
19
  isConnected: boolean;
20
  tools: MCPTool[];
 
 
21
  lastError?: string;
22
  lastConnected?: Date;
23
  }
 
1
+ import type {
2
+ Tool as MCPTool,
3
+ Resource as MCPResource,
4
+ Prompt as MCPPrompt
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
 
7
  export interface MCPServerConfig {
8
  id: string;
 
22
  config: MCPServerConfig;
23
  isConnected: boolean;
24
  tools: MCPTool[];
25
+ resources: MCPResource[];
26
+ prompts: MCPPrompt[];
27
  lastError?: string;
28
  lastConnected?: Date;
29
  }