fa-mcp-sdk 0.4.3 → 0.4.6

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.
Files changed (153) hide show
  1. package/bin/fa-mcp.js +1040 -1039
  2. package/cli-template/eslint.config.js +16 -136
  3. package/cli-template/package.json +9 -10
  4. package/cli-template/tsconfig.json +1 -0
  5. package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
  6. package/dist/core/_types_/config.d.ts +1 -1
  7. package/dist/core/_types_/config.d.ts.map +1 -1
  8. package/dist/core/_types_/types.d.ts.map +1 -1
  9. package/dist/core/ad/group-checker.d.ts.map +1 -1
  10. package/dist/core/ad/group-checker.js.map +1 -1
  11. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  12. package/dist/core/agent-tester/agent-tester-router.js +8 -8
  13. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  14. package/dist/core/agent-tester/check-llm.d.ts.map +1 -1
  15. package/dist/core/agent-tester/check-llm.js +1 -1
  16. package/dist/core/agent-tester/check-llm.js.map +1 -1
  17. package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
  18. package/dist/core/agent-tester/services/TesterAgentService.js +53 -53
  19. package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
  20. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  21. package/dist/core/agent-tester/services/TesterMcpClientService.js +2 -2
  22. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  23. package/dist/core/auth/admin-auth.d.ts.map +1 -1
  24. package/dist/core/auth/admin-auth.js +3 -3
  25. package/dist/core/auth/admin-auth.js.map +1 -1
  26. package/dist/core/auth/basic.d.ts.map +1 -1
  27. package/dist/core/auth/basic.js.map +1 -1
  28. package/dist/core/auth/jwt.d.ts.map +1 -1
  29. package/dist/core/auth/jwt.js +6 -16
  30. package/dist/core/auth/jwt.js.map +1 -1
  31. package/dist/core/auth/middleware.d.ts.map +1 -1
  32. package/dist/core/auth/middleware.js +3 -2
  33. package/dist/core/auth/middleware.js.map +1 -1
  34. package/dist/core/auth/multi-auth.d.ts +0 -3
  35. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  36. package/dist/core/auth/multi-auth.js +10 -7
  37. package/dist/core/auth/multi-auth.js.map +1 -1
  38. package/dist/core/auth/permanent.d.ts.map +1 -1
  39. package/dist/core/auth/permanent.js +1 -1
  40. package/dist/core/auth/permanent.js.map +1 -1
  41. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.d.ts.map +1 -1
  42. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js +2 -2
  43. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js.map +1 -1
  44. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
  45. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +1 -1
  46. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
  47. package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
  48. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +1 -1
  49. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
  50. package/dist/core/auth/token-generator/ntlm/ntlm-templates.d.ts.map +1 -1
  51. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js +222 -221
  52. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js.map +1 -1
  53. package/dist/core/auth/token-generator/server.d.ts.map +1 -1
  54. package/dist/core/auth/token-generator/server.js +8 -8
  55. package/dist/core/auth/token-generator/server.js.map +1 -1
  56. package/dist/core/bootstrap/init-config.d.ts.map +1 -1
  57. package/dist/core/bootstrap/init-config.js +4 -4
  58. package/dist/core/bootstrap/init-config.js.map +1 -1
  59. package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
  60. package/dist/core/bootstrap/startup-info.js +4 -4
  61. package/dist/core/bootstrap/startup-info.js.map +1 -1
  62. package/dist/core/cache/cache.d.ts.map +1 -1
  63. package/dist/core/cache/cache.js +3 -3
  64. package/dist/core/cache/cache.js.map +1 -1
  65. package/dist/core/consul/access-points-updater.d.ts.map +1 -1
  66. package/dist/core/consul/access-points-updater.js +3 -3
  67. package/dist/core/consul/access-points-updater.js.map +1 -1
  68. package/dist/core/consul/deregister.d.ts.map +1 -1
  69. package/dist/core/consul/deregister.js +1 -1
  70. package/dist/core/consul/deregister.js.map +1 -1
  71. package/dist/core/consul/get-consul-api.d.ts.map +1 -1
  72. package/dist/core/consul/get-consul-api.js +3 -3
  73. package/dist/core/consul/get-consul-api.js.map +1 -1
  74. package/dist/core/db/pg-db.d.ts +1 -1
  75. package/dist/core/db/pg-db.d.ts.map +1 -1
  76. package/dist/core/db/pg-db.js +2 -2
  77. package/dist/core/db/pg-db.js.map +1 -1
  78. package/dist/core/debug.js +1 -1
  79. package/dist/core/debug.js.map +1 -1
  80. package/dist/core/init-mcp-server.d.ts.map +1 -1
  81. package/dist/core/init-mcp-server.js +9 -9
  82. package/dist/core/init-mcp-server.js.map +1 -1
  83. package/dist/core/logger.d.ts.map +1 -1
  84. package/dist/core/logger.js +3 -3
  85. package/dist/core/logger.js.map +1 -1
  86. package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
  87. package/dist/core/mcp/create-mcp-server.js +1 -1
  88. package/dist/core/mcp/create-mcp-server.js.map +1 -1
  89. package/dist/core/mcp/prompts.d.ts.map +1 -1
  90. package/dist/core/mcp/prompts.js +1 -3
  91. package/dist/core/mcp/prompts.js.map +1 -1
  92. package/dist/core/mcp/resources.d.ts.map +1 -1
  93. package/dist/core/mcp/resources.js +8 -10
  94. package/dist/core/mcp/resources.js.map +1 -1
  95. package/dist/core/mcp/server-stdio.d.ts.map +1 -1
  96. package/dist/core/mcp/server-stdio.js.map +1 -1
  97. package/dist/core/utils/formatToolResult.d.ts.map +1 -1
  98. package/dist/core/utils/formatToolResult.js +1 -3
  99. package/dist/core/utils/formatToolResult.js.map +1 -1
  100. package/dist/core/utils/port-checker.d.ts.map +1 -1
  101. package/dist/core/utils/port-checker.js +1 -1
  102. package/dist/core/utils/port-checker.js.map +1 -1
  103. package/dist/core/utils/rate-limit.js +2 -2
  104. package/dist/core/utils/testing/McpSseClient.d.ts.map +1 -1
  105. package/dist/core/utils/testing/McpSseClient.js.map +1 -1
  106. package/dist/core/utils/testing/McpStdioClient.d.ts.map +1 -1
  107. package/dist/core/utils/testing/McpStdioClient.js.map +1 -1
  108. package/dist/core/utils/utils.d.ts.map +1 -1
  109. package/dist/core/utils/utils.js.map +1 -1
  110. package/dist/core/web/admin-router.d.ts.map +1 -1
  111. package/dist/core/web/admin-router.js +4 -4
  112. package/dist/core/web/admin-router.js.map +1 -1
  113. package/dist/core/web/cors.d.ts.map +1 -1
  114. package/dist/core/web/cors.js.map +1 -1
  115. package/dist/core/web/favicon-svg.d.ts.map +1 -1
  116. package/dist/core/web/favicon-svg.js.map +1 -1
  117. package/dist/core/web/home-api.d.ts.map +1 -1
  118. package/dist/core/web/home-api.js +4 -4
  119. package/dist/core/web/home-api.js.map +1 -1
  120. package/dist/core/web/openapi.d.ts.map +1 -1
  121. package/dist/core/web/openapi.js.map +1 -1
  122. package/dist/core/web/server-http.d.ts.map +1 -1
  123. package/dist/core/web/server-http.js +20 -22
  124. package/dist/core/web/server-http.js.map +1 -1
  125. package/dist/core/web/static/agent-tester/script.js +1503 -1513
  126. package/dist/core/web/static/home/script.js +646 -646
  127. package/dist/core/web/static/token-gen/script.js +561 -561
  128. package/dist/core/web/svg-icons.d.ts.map +1 -1
  129. package/dist/core/web/svg-icons.js +1 -1
  130. package/dist/core/web/svg-icons.js.map +1 -1
  131. package/package.json +2 -6
  132. package/scripts/copy-static.js +31 -31
  133. package/scripts/kill-port.js +107 -107
  134. package/scripts/npm/patch_node_modules.js +8 -8
  135. package/scripts/npm/run.js +31 -31
  136. package/scripts/remove-nul.js +53 -53
  137. package/scripts/update-doc.js +18 -18
  138. package/src/template/_types_/custom-config.ts +83 -83
  139. package/src/template/api/router.ts +86 -89
  140. package/src/template/custom-resources.ts +11 -11
  141. package/src/template/prompts/agent-brief.ts +8 -8
  142. package/src/template/prompts/agent-prompt.ts +10 -10
  143. package/src/template/prompts/custom-prompts.ts +12 -12
  144. package/src/template/start.ts +71 -72
  145. package/src/template/tools/handle-tool-call.ts +57 -56
  146. package/src/template/tools/tools.ts +89 -88
  147. package/src/tests/jest-simple-reporter.js +10 -10
  148. package/src/tests/mcp/sse/test-sse-npm-package.js +96 -96
  149. package/src/tests/mcp/test-cases.js +143 -143
  150. package/src/tests/mcp/test-http.js +76 -75
  151. package/src/tests/mcp/test-sse.js +80 -79
  152. package/src/tests/mcp/test-stdio.js +83 -81
  153. package/src/tests/utils.ts +157 -156
@@ -1,1513 +1,1503 @@
1
- const API_BASE = '/agent-tester';
2
- const trim = (s) => String(s || '').trim();
3
-
4
- class McpAgentTester {
5
- constructor () {
6
- this.currentSessionId = null;
7
- this.currentServer = null;
8
- this.currentSystemPrompt = '';
9
- this.usedHeaders = [];
10
- this.pendingConnectionData = null;
11
- this._headersUpdateTimer = null;
12
- this.defaultMcpUrl = null;
13
- this.authEnabled = false;
14
- this.configHttpHeaders = {};
15
- this._authRefreshInterval = null;
16
- this._currentAuthType = null;
17
- this.messageFormats = {};
18
- this.messageTexts = {};
19
- this.defaultDisplayFormat = localStorage.getItem('agentTesterDefaultFormat') || 'HTML';
20
-
21
- this.mcpConfig = {
22
- url: null,
23
- transport: 'http',
24
- headers: {},
25
- name: null,
26
- };
27
-
28
- this.initializeElements();
29
- this.initTheme();
30
- this.bindEvents();
31
- this.loadInitialData();
32
-
33
- this.setupAutoResize();
34
-
35
- console.log('MCP Agent Tester initialized');
36
- }
37
-
38
- sanitizeHtml (html) {
39
- const allowedTags = [
40
- 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'code', 'pre',
41
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
42
- 'ul', 'ol', 'li', 'blockquote', 'a', 'span', 'div',
43
- 'table', 'thead', 'tbody', 'tr', 'th', 'td',
44
- ];
45
-
46
- const allowedAttributes = {
47
- 'a': ['href', 'title', 'target'],
48
- 'th': ['colspan', 'rowspan'],
49
- 'td': ['colspan', 'rowspan'],
50
- 'code': ['class'],
51
- 'pre': ['class'],
52
- 'span': ['class'],
53
- 'div': ['class'],
54
- };
55
-
56
- const tempDiv = document.createElement('div');
57
- tempDiv.innerHTML = html;
58
-
59
- const cleanNode = (node) => {
60
- if (node.nodeType === Node.TEXT_NODE) {
61
- return node;
62
- }
63
-
64
- if (node.nodeType !== Node.ELEMENT_NODE) {
65
- return null;
66
- }
67
-
68
- const tagName = node.tagName.toLowerCase();
69
-
70
- if (!allowedTags.includes(tagName)) {
71
- const textNode = document.createTextNode(node.textContent || '');
72
- return textNode;
73
- }
74
-
75
- const cleanedElement = document.createElement(tagName);
76
-
77
- const allowedAttrs = allowedAttributes[tagName] || [];
78
- allowedAttrs.forEach(attr => {
79
- if (node.hasAttribute(attr)) {
80
- const value = node.getAttribute(attr);
81
- if (attr === 'href') {
82
- try {
83
- const url = new URL(value, window.location.href);
84
- if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
85
- cleanedElement.setAttribute(attr, value);
86
- }
87
- } catch {
88
- // Invalid URL, skip
89
- }
90
- } else {
91
- cleanedElement.setAttribute(attr, value);
92
- }
93
- }
94
- });
95
-
96
- Array.from(node.childNodes).forEach(child => {
97
- const cleanedChild = cleanNode(child);
98
- if (cleanedChild) {
99
- cleanedElement.appendChild(cleanedChild);
100
- }
101
- });
102
-
103
- return cleanedElement;
104
- };
105
-
106
- const cleanedNodes = Array.from(tempDiv.childNodes).map(cleanNode).filter(node => node !== null);
107
-
108
- const finalDiv = document.createElement('div');
109
- cleanedNodes.forEach(node => finalDiv.appendChild(node));
110
-
111
- return finalDiv.innerHTML.trim();
112
- }
113
-
114
- createFormatToggle (messageId) {
115
- const toggleContainer = document.createElement('div');
116
- toggleContainer.className = 'format-toggle-container';
117
-
118
- const select = document.createElement('select');
119
- select.className = 'format-toggle';
120
- select.dataset.messageId = messageId;
121
-
122
- const options = ['MD', 'HTML'];
123
- const currentFormat = this.messageFormats[messageId] || 'MD';
124
-
125
- options.forEach(opt => {
126
- const option = document.createElement('option');
127
- option.value = opt;
128
- option.textContent = opt;
129
- if (opt === currentFormat) {
130
- option.selected = true;
131
- }
132
- select.appendChild(option);
133
- });
134
-
135
- select.addEventListener('change', (e) => {
136
- this.onFormatChange(messageId, e.target.value);
137
- });
138
-
139
- toggleContainer.appendChild(select);
140
- return toggleContainer;
141
- }
142
-
143
- onFormatChange (messageId, format) {
144
- this.messageFormats[messageId] = format;
145
- const originalText = this.messageTexts[messageId];
146
- const messageText = document.querySelector(`.message-text[data-message-id="${messageId}"]`);
147
- if (messageText && originalText) {
148
- this.renderMessageContent(messageText, originalText, format);
149
- }
150
- }
151
-
152
- handleDefaultFormatChange () {
153
- const value = this.defaultFormatSelect.value;
154
- this.defaultDisplayFormat = value;
155
- localStorage.setItem('agentTesterDefaultFormat', value);
156
- if (value === 'HTML') {
157
- this.showToast('Tip: add "Format your response in HTML" to Custom Prompt for best results', 'info');
158
- }
159
- }
160
-
161
- renderMessageContent (element, text, format) {
162
- if (format === 'HTML') {
163
- element.innerHTML = this.sanitizeHtml(text).trim();
164
- element.classList.add('html-content');
165
- element.classList.remove('md-content');
166
- } else {
167
- element.textContent = text;
168
- element.classList.add('md-content');
169
- element.classList.remove('html-content');
170
- }
171
- }
172
-
173
- initializeElements () {
174
- this.sidebar = document.getElementById('sidebar');
175
- this.sidebarToggle = document.getElementById('sidebarToggle');
176
- this.sidebarToggleMobile = document.getElementById('sidebarToggleMobile');
177
-
178
- this.mcpConnectionForm = document.getElementById('mcpConnectionForm');
179
- this.serverUrlInput = document.getElementById('serverUrl');
180
- this.transportSelect = document.getElementById('transport');
181
-
182
- this.serverUrlDropdown = document.getElementById('serverUrlDropdown');
183
- this.serverUrlDropdownList = document.getElementById('serverUrlDropdownList');
184
- this.savedUrlsList = document.getElementById('savedUrlsList');
185
-
186
- this.currentServer = null;
187
-
188
- this.headersSection = document.getElementById('headersSection');
189
- this.dynamicHeaders = document.getElementById('dynamicHeaders');
190
-
191
- this.modelSelect = document.getElementById('modelSelect');
192
-
193
- this.customModelSettings = document.getElementById('customModelSettings');
194
- this.customBaseUrl = document.getElementById('customBaseUrl');
195
- this.customApiKey = document.getElementById('customApiKey');
196
- this.customModelName = document.getElementById('customModelName');
197
- this.modelTemperature = document.getElementById('modelTemperature');
198
- this.modelMaxTokens = document.getElementById('modelMaxTokens');
199
- this.modelMaxTurns = document.getElementById('modelMaxTurns');
200
- this.toolResultLimitChars = document.getElementById('toolResultLimitChars');
201
-
202
- this.systemPromptTextarea = document.getElementById('systemPrompt');
203
- this.customPromptTextarea = document.getElementById('customPrompt');
204
-
205
- this.connectedServersContainer = document.getElementById('connectedServers');
206
-
207
- this.chatMessages = document.getElementById('chatMessages');
208
- this.messageInput = document.getElementById('messageInput');
209
- this.sendButton = document.getElementById('sendButton');
210
- this.clearChatBtn = document.getElementById('clearChat');
211
- this.connectionStatus = document.getElementById('connectionStatus');
212
- this.charCount = document.getElementById('charCount');
213
- this.typingIndicator = document.getElementById('typingIndicator');
214
-
215
- this.loadingOverlay = document.getElementById('loadingOverlay');
216
- this.toastContainer = document.getElementById('toastContainer');
217
-
218
- this.themeToggle = document.getElementById('themeToggle');
219
- this.defaultFormatSelect = document.getElementById('defaultDisplayFormat');
220
- }
221
-
222
- bindEvents () {
223
- if (this.sidebarToggle) {
224
- this.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
225
- }
226
- if (this.sidebarToggleMobile) {
227
- this.sidebarToggleMobile.addEventListener('click', () => this.toggleSidebar());
228
- }
229
-
230
- if (this.themeToggle) {
231
- this.themeToggle.addEventListener('click', () => this.toggleTheme());
232
- }
233
-
234
- if (this.defaultFormatSelect) {
235
- this.defaultFormatSelect.value = this.defaultDisplayFormat;
236
- this.defaultFormatSelect.addEventListener('change', () => this.handleDefaultFormatChange());
237
- }
238
-
239
- this.mcpConnectionForm.addEventListener('submit', (e) => this.handleMcpConnection(e));
240
-
241
- this.serverUrlInput.addEventListener('input', () => this.handleServerUrlChange());
242
- this.transportSelect.addEventListener('change', () => this.saveFormValuesToStorage());
243
-
244
- this.serverUrlDropdown.addEventListener('click', (e) => this.toggleUrlDropdown(e));
245
- document.addEventListener('click', (e) => this.handleClickOutside(e));
246
-
247
- this.modelSelect.addEventListener('change', () => {
248
- this.handleModelSelectChange();
249
- this.saveFormValuesToStorage();
250
- });
251
- this.systemPromptTextarea.addEventListener('input', () => this.saveFormValuesToStorage());
252
- this.customPromptTextarea.addEventListener('input', () => this.saveFormValuesToStorage());
253
-
254
- this.customBaseUrl.addEventListener('input', () => this.saveFormValuesToStorage());
255
- this.customApiKey.addEventListener('input', () => this.saveFormValuesToStorage());
256
- this.customModelName.addEventListener('input', () => this.saveFormValuesToStorage());
257
- this.modelTemperature.addEventListener('input', () => this.saveFormValuesToStorage());
258
- this.modelMaxTokens.addEventListener('input', () => this.saveFormValuesToStorage());
259
- this.modelMaxTurns.addEventListener('input', () => this.saveFormValuesToStorage());
260
- this.toolResultLimitChars.addEventListener('input', () => this.saveFormValuesToStorage());
261
-
262
- document.querySelectorAll('.btn-enlarge').forEach(btn => {
263
- btn.addEventListener('click', () => this.openPromptModal(btn.dataset.target));
264
- });
265
- document.getElementById('promptModalClose').addEventListener('click', () => this.closePromptModal());
266
- document.getElementById('promptModalSave').addEventListener('click', () => this.savePromptModal());
267
- document.getElementById('promptModal').addEventListener('click', (e) => {
268
- if (e.target === e.currentTarget) {this.closePromptModal();}
269
- });
270
-
271
- this.messageInput.addEventListener('input', () => this.handleInputChange());
272
- this.messageInput.addEventListener('keydown', (e) => this.handleKeyDown(e));
273
- this.sendButton.addEventListener('click', () => this.sendMessage());
274
- this.clearChatBtn.addEventListener('click', () => this.clearChat());
275
-
276
- document.addEventListener('click', (e) => {
277
- if (window.innerWidth <= 768 &&
278
- !this.sidebar.contains(e.target) &&
279
- !this.sidebarToggleMobile.contains(e.target) &&
280
- this.sidebar.classList.contains('open')) {
281
- this.toggleSidebar();
282
- }
283
- });
284
-
285
- window.addEventListener('resize', () => {
286
- if (window.innerWidth > 768) {
287
- this.sidebar.classList.remove('open');
288
- }
289
- });
290
- }
291
-
292
- initTheme () {
293
- const saved = localStorage.getItem('mcpAgentTheme');
294
- let theme = saved;
295
- if (!theme) {
296
- theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
297
- ? 'dark' : 'light';
298
- }
299
- this.applyTheme(theme);
300
- }
301
-
302
- toggleTheme () {
303
- const current = document.documentElement.getAttribute('data-theme') || 'light';
304
- const next = current === 'dark' ? 'light' : 'dark';
305
- this.applyTheme(next);
306
- localStorage.setItem('mcpAgentTheme', next);
307
- }
308
-
309
- applyTheme (theme) {
310
- document.documentElement.setAttribute('data-theme', theme);
311
- if (this.themeToggle) {
312
- const icon = this.themeToggle.querySelector('.material-icons-round');
313
- if (icon) {
314
- icon.textContent = theme === 'dark' ? 'light_mode' : 'dark_mode';
315
- }
316
- }
317
- }
318
-
319
- openPromptModal (targetId) {
320
- this._promptModalTarget = document.getElementById(targetId);
321
- const modal = document.getElementById('promptModal');
322
- const textarea = document.getElementById('promptModalTextarea');
323
- const title = document.getElementById('promptModalTitle');
324
- title.textContent = targetId === 'systemPrompt' ? 'Agent Prompt' : 'Custom Prompt';
325
- textarea.value = this._promptModalTarget.value;
326
- modal.style.display = 'flex';
327
- textarea.focus();
328
- }
329
-
330
- closePromptModal () {
331
- document.getElementById('promptModal').style.display = 'none';
332
- this._promptModalTarget = null;
333
- }
334
-
335
- savePromptModal () {
336
- if (this._promptModalTarget) {
337
- this._promptModalTarget.value = document.getElementById('promptModalTextarea').value;
338
- this.saveFormValuesToStorage();
339
- }
340
- this.closePromptModal();
341
- }
342
-
343
- setupAutoResize () {
344
- this.messageInput.addEventListener('input', function () {
345
- this.style.height = 'auto';
346
- this.style.height = Math.min(this.scrollHeight, 120) + 'px';
347
- });
348
- }
349
-
350
- toggleSidebar () {
351
- this.sidebar.classList.toggle('open');
352
- }
353
-
354
- async loadInitialData () {
355
- try {
356
- this.loadFormValuesFromStorage();
357
- this.loadFormValuesFromURL();
358
- this.handleServerUrlChange();
359
- this.renderSavedUrls();
360
- await this.loadDefaultConfig();
361
- await this.loadCurrentServer();
362
- this.currentSystemPrompt = this.systemPromptTextarea.value;
363
-
364
- const serverUrl = this.serverUrlInput.value.trim();
365
- if (serverUrl && (!this.currentServer || !this.currentServer.isConnected)) {
366
- // Auto-connect if there's a URL but no connected server
367
- await this.autoConnect();
368
- } else if (serverUrl && this.currentServer && this.currentServer.isConnected) {
369
- // Already connected — still need to load headers
370
- await this.checkRequiredHeaders();
371
- }
372
- } catch (error) {
373
- console.error('Error loading initial data:', error);
374
- }
375
- }
376
-
377
- async autoConnect () {
378
- const serverUrl = this.serverUrlInput.value.trim();
379
- if (!serverUrl) {return;}
380
-
381
- const transport = this.transportSelect.value;
382
- const serverName = this.generateServerName(serverUrl);
383
-
384
- const connectionData = {
385
- name: serverName,
386
- url: serverUrl,
387
- transport: transport,
388
- headers: this.getHeadersFromForm(),
389
- };
390
-
391
- this.showLoading('Auto-connecting to MCP server...');
392
-
393
- try {
394
- const response = await fetch(`${API_BASE}/api/mcp/connect`, {
395
- method: 'POST',
396
- headers: { 'Content-Type': 'application/json' },
397
- body: JSON.stringify(connectionData),
398
- });
399
-
400
- const result = await response.json();
401
-
402
- if (result.success) {
403
- this.currentServer = {
404
- name: serverName,
405
- url: serverUrl,
406
- isConnected: true,
407
- ...result.config,
408
- };
409
-
410
- this.mcpConfig = {
411
- url: serverUrl,
412
- transport: transport,
413
- headers: this.getHeadersFromForm(),
414
- name: serverName,
415
- };
416
-
417
- if (result.config && result.config.agentPrompt) {
418
- this.systemPromptTextarea.value = result.config.agentPrompt;
419
- this.currentSystemPrompt = result.config.agentPrompt;
420
- }
421
-
422
- this.addUrlToSaved(serverUrl);
423
- this.headersSection.style.display = 'none';
424
- this.dynamicHeaders.innerHTML = '';
425
- this.usedHeaders = [];
426
- this.updateConnectionStatus();
427
- this.renderServerInfo();
428
-
429
- await this.checkRequiredHeaders();
430
-
431
- this.showToast('Auto-connected to ' + serverName, 'success');
432
- } else {
433
- console.warn('Auto-connect failed:', result.error);
434
- }
435
- } catch (error) {
436
- console.warn('Auto-connect failed:', error.message);
437
- } finally {
438
- this.hideLoading();
439
- }
440
- }
441
-
442
- async loadDefaultConfig () {
443
- try {
444
- const response = await fetch(`${API_BASE}/api/config`);
445
- const config = await response.json();
446
- this.defaultMcpUrl = config.defaultMcpUrl || null;
447
- this.authEnabled = !!config.authEnabled;
448
- this.configHttpHeaders = config.httpHeaders || {};
449
- if (config.defaultMcpUrl) {
450
- const serverUrlInput = document.getElementById('serverUrl');
451
- if (!this.mcpConfig.url && !serverUrlInput.value) {
452
- serverUrlInput.value = config.defaultMcpUrl;
453
- }
454
- }
455
- } catch (e) {
456
- console.warn('Failed to load default config:', e);
457
- }
458
- }
459
-
460
- async handleMcpConnection (event) {
461
- event.preventDefault();
462
-
463
- const serverUrl = this.serverUrlInput.value.trim();
464
- const transport = this.transportSelect.value;
465
-
466
- const serverName = this.generateServerName(serverUrl);
467
-
468
- const connectionData = {
469
- name: serverName,
470
- url: serverUrl,
471
- transport: transport,
472
- headers: this.getHeadersFromForm(),
473
- };
474
-
475
- this.showLoading('Connecting to MCP server...');
476
-
477
- try {
478
- const response = await fetch(`${API_BASE}/api/mcp/connect`, {
479
- method: 'POST',
480
- headers: {
481
- 'Content-Type': 'application/json',
482
- },
483
- body: JSON.stringify(connectionData),
484
- });
485
-
486
- const result = await response.json();
487
-
488
- if (result.success) {
489
- this.currentServer = {
490
- name: serverName,
491
- url: serverUrl,
492
- isConnected: true,
493
- ...result.config,
494
- };
495
-
496
- this.mcpConfig = {
497
- url: serverUrl,
498
- transport: transport,
499
- headers: this.getHeadersFromForm(),
500
- name: serverName,
501
- };
502
-
503
- if (result.config && result.config.agentPrompt) {
504
- this.systemPromptTextarea.value = result.config.agentPrompt;
505
- this.currentSystemPrompt = result.config.agentPrompt;
506
- }
507
-
508
- this.showToast('Successfully connected to ' + serverName, 'success');
509
-
510
- this.addUrlToSaved(serverUrl);
511
-
512
- this.headersSection.style.display = 'none';
513
- this.dynamicHeaders.innerHTML = '';
514
- this.usedHeaders = [];
515
-
516
- this.updateConnectionStatus();
517
- this.renderServerInfo();
518
-
519
- await this.checkRequiredHeaders();
520
- } else {
521
- this.showToast('Failed to connect: ' + result.error, 'error');
522
- }
523
-
524
- } catch (error) {
525
- console.error('Connection error:', error);
526
- this.showToast('Connection failed: ' + error.message, 'error');
527
- } finally {
528
- this.hideLoading();
529
- }
530
- }
531
-
532
- generateServerName (url) {
533
- try {
534
- const parsedUrl = new URL(url);
535
- const { hostname, port } = parsedUrl;
536
-
537
- let serverName = hostname;
538
- serverName = serverName.replace(/^www\./, '').split('.')[0] || 'MCP Server';
539
-
540
- if (port && port !== '80' && port !== '443') {
541
- serverName += ':' + port;
542
- }
543
- return serverName;
544
- } catch {
545
- return url.split('/')[2] || 'MCP Server';
546
- }
547
- }
548
-
549
- async checkRequiredHeaders () {
550
- const url = this.serverUrlInput.value.trim();
551
-
552
- if (!url) {
553
- this.showToast('Please enter a server URL first', 'warning');
554
- return;
555
- }
556
-
557
- this.showLoading('Checking used headers...');
558
-
559
- try {
560
- const response = await fetch(`${API_BASE}/api/mcp/used-headers?url=${encodeURIComponent(url)}`, {
561
- method: 'GET',
562
- headers: {
563
- 'Accept': 'application/json',
564
- },
565
- });
566
-
567
- if (response.ok) {
568
- const headers = await response.json();
569
- this.usedHeaders = Array.isArray(headers) ? headers : [];
570
- this.renderHeaderInputs();
571
- await this.autoFillAuthHeader();
572
-
573
- if (this.usedHeaders.length > 0) {
574
- const reqCount = this.usedHeaders.filter(h => !h.isOptional).length;
575
- this.showToast(`Found ${this.usedHeaders.length} headers (${reqCount} used)`, 'success');
576
- this.headersSection.style.display = 'block';
577
- } else {
578
- this.showToast('No additional headers used', 'info');
579
- this.headersSection.style.display = 'none';
580
- }
581
- } else {
582
- this.showToast('Headers endpoint not available - proceeding without additional headers', 'info');
583
- this.headersSection.style.display = 'none';
584
- this.usedHeaders = [];
585
- }
586
-
587
- } catch (error) {
588
- console.log('Headers check failed:', error);
589
- this.showToast('Headers endpoint not available - proceeding without additional headers', 'info');
590
- this.headersSection.style.display = 'none';
591
- this.usedHeaders = [];
592
- } finally {
593
- this.hideLoading();
594
- }
595
- }
596
-
597
- renderHeaderInputs () {
598
- this.dynamicHeaders.innerHTML = '';
599
- const savedHeaders = this.loadHeaderValuesFromStorage();
600
-
601
- this.usedHeaders.forEach(header => {
602
- const headerGroup = document.createElement('div');
603
- headerGroup.className = 'header-row';
604
-
605
- const savedValue = savedHeaders[header.name] || this.configHttpHeaders[header.name] || '';
606
- const isRequired = !header.isOptional;
607
- const hasDesc = header.description && header.description.trim();
608
- const nameClass = hasDesc ? 'header-name has-tooltip' : 'header-name';
609
- const tooltipAttr = hasDesc ? ` data-tooltip="${header.description.replace(/"/g, '&quot;')}"` : '';
610
- const inputClass = isRequired ? 'header-value used-header' : 'header-value';
611
-
612
- headerGroup.innerHTML = `
613
- <span class="${nameClass}"${tooltipAttr}>${header.name}</span>
614
- <input
615
- type="text"
616
- class="${inputClass}"
617
- id="header_${header.name}"
618
- placeholder="${header.name}"
619
- data-header-name="${header.name}"
620
- data-required="${isRequired}"
621
- value="${savedValue.replace(/"/g, '&quot;')}"
622
- >
623
- `;
624
-
625
- this.dynamicHeaders.appendChild(headerGroup);
626
-
627
- const nameEl = headerGroup.querySelector('.header-name');
628
- if (nameEl && hasDesc) {
629
- nameEl.style.cursor = 'pointer';
630
- nameEl.addEventListener('click', (e) => {
631
- e.stopPropagation();
632
- this.toggleHeaderTooltip(e, header.description);
633
- });
634
- }
635
-
636
- const inputEl = headerGroup.querySelector(`#header_${header.name}`);
637
- if (inputEl) {
638
- inputEl.addEventListener('input', () => {
639
- this.saveHeaderValuesToStorage();
640
- this.scheduleHeadersUpdate();
641
- this.updateHeaderBorder(inputEl);
642
- });
643
- this.updateHeaderBorder(inputEl);
644
- }
645
- });
646
-
647
- this.mcpConfig.headers = this.getHeadersFromForm();
648
- }
649
-
650
- toggleHeaderTooltip (e, text) {
651
- const tip = document.getElementById('headerTooltip');
652
- if (tip.classList.contains('visible') && tip._sourceEl === e.target) {
653
- this.hideHeaderTooltip();
654
- return;
655
- }
656
- tip._sourceEl = e.target;
657
- tip.textContent = text;
658
- const rect = e.target.getBoundingClientRect();
659
- tip.style.left = rect.left + 'px';
660
- tip.style.top = (rect.top - 4) + 'px';
661
- tip.style.transform = 'translateY(-100%)';
662
- tip.classList.add('visible');
663
-
664
- const dismissOnClick = (ev) => {
665
- if (ev.target !== e.target && !tip.contains(ev.target)) {
666
- this.hideHeaderTooltip();
667
- document.removeEventListener('click', dismissOnClick);
668
- }
669
- };
670
- setTimeout(() => document.addEventListener('click', dismissOnClick), 0);
671
- }
672
-
673
- hideHeaderTooltip () {
674
- const tip = document.getElementById('headerTooltip');
675
- tip.classList.remove('visible');
676
- tip._sourceEl = null;
677
- }
678
-
679
- updateHeaderBorder (inputEl) {
680
- if (inputEl.dataset.required === 'true') {
681
- if (inputEl.value.trim()) {
682
- inputEl.classList.remove('empty-required');
683
- } else {
684
- inputEl.classList.add('empty-required');
685
- }
686
- }
687
- }
688
-
689
- getHeaderStorageKey () {
690
- const url = this.serverUrlInput.value.trim();
691
- return `mcpHeaderValues_${url}`;
692
- }
693
-
694
- saveHeaderValuesToStorage () {
695
- const headers = this.getHeadersFromForm();
696
- const key = this.getHeaderStorageKey();
697
- try {
698
- localStorage.setItem(key, JSON.stringify(headers));
699
- } catch (error) {
700
- console.error('Error saving header values to storage:', error);
701
- }
702
- }
703
-
704
- loadHeaderValuesFromStorage () {
705
- const key = this.getHeaderStorageKey();
706
- try {
707
- const stored = localStorage.getItem(key);
708
- return stored ? JSON.parse(stored) : {};
709
- } catch (error) {
710
- console.error('Error loading header values from storage:', error);
711
- return {};
712
- }
713
- }
714
-
715
- scheduleHeadersUpdate () {
716
- this.mcpConfig.headers = this.getHeadersFromForm();
717
-
718
- if (this._headersUpdateTimer) {
719
- clearTimeout(this._headersUpdateTimer);
720
- }
721
- this._headersUpdateTimer = setTimeout(() => {
722
- this.applyHeadersUpdate().catch(err => console.warn('Apply headers failed:', err));
723
- }, 600);
724
- }
725
-
726
- async applyHeadersUpdate () {
727
- if (!this.currentServer || !this.currentServer.name) {
728
- return;
729
- }
730
- const headers = this.getHeadersFromForm();
731
- try {
732
- const resp = await fetch(`${API_BASE}/api/mcp/headers`, {
733
- method: 'POST',
734
- headers: { 'Content-Type': 'application/json' },
735
- body: JSON.stringify({ serverName: this.currentServer.name, headers }),
736
- });
737
- const data = await resp.json();
738
- if (!resp.ok || !data.success) {
739
- this.showToast('Failed to apply headers: ' + (data.error || resp.statusText), 'error');
740
- return;
741
- }
742
- if (data.config) {
743
- this.currentServer = { ...this.currentServer, ...data.config };
744
- this.renderServerInfo();
745
- }
746
- this.showToast('Headers applied', 'success');
747
- } catch (e) {
748
- this.showToast('Failed to apply headers: ' + (e?.message || e), 'error');
749
- }
750
- }
751
-
752
- getHeadersFromForm () {
753
- const headers = {};
754
-
755
- if (this.usedHeaders.length === 0) {
756
- return headers;
757
- }
758
-
759
- this.usedHeaders.forEach(header => {
760
- const input = document.getElementById(`header_${header.name}`);
761
- if (input && input.value.trim()) {
762
- headers[header.name] = input.value.trim();
763
- }
764
- });
765
-
766
- return headers;
767
- }
768
-
769
- isOwnService () {
770
- return this.defaultMcpUrl && this.serverUrlInput.value.trim() === this.defaultMcpUrl;
771
- }
772
-
773
- async autoFillAuthHeader () {
774
- if (!this.authEnabled) {return;}
775
-
776
- const hasAuthHeader = this.usedHeaders.some(h => h.name === 'Authorization');
777
- if (!hasAuthHeader) {return;}
778
-
779
- // Skip if localStorage already has a saved value for this URL's Authorization header
780
- const savedHeaders = this.loadHeaderValuesFromStorage();
781
- if (savedHeaders['Authorization']) {return;}
782
-
783
- try {
784
- const response = await fetch(`${API_BASE}/api/auth-token`);
785
- if (!response.ok) {return;}
786
-
787
- const data = await response.json();
788
- this._currentAuthType = data.authType;
789
-
790
- const input = document.getElementById('header_Authorization');
791
- if (input) {
792
- input.value = data.token;
793
- this.updateHeaderBorder(input);
794
- this.saveHeaderValuesToStorage();
795
- this.scheduleHeadersUpdate();
796
- }
797
-
798
- // Start JWT refresh interval if connecting to own service
799
- if (data.authType === 'jwtToken' && this.isOwnService()) {
800
- this.startAuthRefresh();
801
- }
802
- } catch (e) {
803
- console.warn('Failed to auto-fill auth header:', e);
804
- }
805
- }
806
-
807
- startAuthRefresh () {
808
- this.stopAuthRefresh();
809
- this._authRefreshInterval = setInterval(async () => {
810
- try {
811
- const response = await fetch(`${API_BASE}/api/auth-token/refresh`, { method: 'POST' });
812
- if (!response.ok) {return;}
813
-
814
- const data = await response.json();
815
- const input = document.getElementById('header_Authorization');
816
- if (input) {
817
- input.value = data.token;
818
- this.saveHeaderValuesToStorage();
819
- this.scheduleHeadersUpdate();
820
- }
821
- } catch (e) {
822
- console.warn('Failed to refresh auth token:', e);
823
- }
824
- }, 4 * 60 * 1000); // every 4 minutes
825
- }
826
-
827
- stopAuthRefresh () {
828
- if (this._authRefreshInterval) {
829
- clearInterval(this._authRefreshInterval);
830
- this._authRefreshInterval = null;
831
- }
832
- }
833
-
834
- resetConnectionForm () {
835
- this.stopAuthRefresh();
836
- this.mcpConnectionForm.reset();
837
- this.serverUrlInput.value = '';
838
- this.transportSelect.value = 'http';
839
- this.headersSection.style.display = 'none';
840
- this.dynamicHeaders.innerHTML = '';
841
- this.usedHeaders = [];
842
- this.pendingConnectionData = null;
843
- this.mcpConfig = {
844
- url: null,
845
- transport: 'http',
846
- headers: {},
847
- name: null,
848
- };
849
- window.history.replaceState({}, document.title, window.location.pathname);
850
- localStorage.removeItem('mcpAgentFormValues');
851
- }
852
-
853
- async loadCurrentServer () {
854
- try {
855
- const response = await fetch(`${API_BASE}/api/mcp/servers`);
856
- const servers = await response.json();
857
-
858
- if (servers && servers.length > 0) {
859
- this.currentServer = servers[0];
860
- this.updateConnectionStatus();
861
- this.renderServerInfo();
862
- } else {
863
- this.currentServer = null;
864
- this.updateConnectionStatus();
865
- this.renderServerInfo();
866
- }
867
-
868
- } catch (error) {
869
- console.error('Error loading current server:', error);
870
- this.currentServer = null;
871
- this.updateConnectionStatus();
872
- this.renderServerInfo();
873
- }
874
- }
875
-
876
- renderServerInfo () {
877
- if (!this.currentServer) {
878
- this.connectedServersContainer.innerHTML = '';
879
- return;
880
- }
881
-
882
- const server = this.currentServer;
883
- const toolCount = server.tools ? server.tools.length : 0;
884
-
885
- if (server.isConnected) {
886
- this.connectedServersContainer.innerHTML = `
887
- <div class="server-status-row">
888
- <span class="server-status connected">${toolCount} tools <span class="material-icons-round">check_circle</span> connected</span>
889
- <button type="button" class="btn btn-danger disconnect-btn"><span class="material-icons-round">link_off</span>Disconnect</button>
890
- </div>`;
891
- } else {
892
- this.connectedServersContainer.innerHTML = `
893
- <div class="server-status-row">
894
- <span class="server-status disconnected"><span class="material-icons-round">cancel</span>Disconnected</span>
895
- <button type="button" class="btn btn-secondary reconnect-btn"><span class="material-icons-round">refresh</span>Reconnect</button>
896
- </div>`;
897
- }
898
-
899
- this.connectedServersContainer.querySelector('.disconnect-btn')?.addEventListener('click', () => {
900
- this.disconnectServer();
901
- });
902
-
903
- this.connectedServersContainer.querySelector('.reconnect-btn')?.addEventListener('click', () => {
904
- this.handleReconnect();
905
- });
906
- }
907
-
908
- async disconnectServer () {
909
- if (!this.currentServer) {
910
- return;
911
- }
912
-
913
- this.stopAuthRefresh();
914
-
915
- try {
916
- const response = await fetch(`${API_BASE}/api/mcp/disconnect/${this.currentServer.name}`, {
917
- method: 'POST',
918
- });
919
-
920
- if (response.ok) {
921
- this.showToast(`Disconnected from ${this.currentServer.name}`, 'success');
922
- this.currentServer = null;
923
- this.mcpConfig = {
924
- url: null,
925
- transport: 'http',
926
- headers: {},
927
- name: null,
928
- };
929
- await this.loadCurrentServer();
930
- this.updateConnectionStatus();
931
- } else {
932
- this.showToast('Failed to disconnect', 'error');
933
- }
934
-
935
- } catch (error) {
936
- console.error('Disconnect error:', error);
937
- this.showToast('Disconnect failed: ' + error.message, 'error');
938
- }
939
- }
940
-
941
- async handleReconnect () {
942
- if (!this.currentServer) {
943
- return;
944
- }
945
-
946
- const connectionData = {
947
- name: this.currentServer.name,
948
- url: this.currentServer.url,
949
- transport: this.currentServer.transport || 'http',
950
- headers: this.currentServer.headers || {},
951
- };
952
-
953
- this.showLoading('Reconnecting to MCP server...');
954
-
955
- try {
956
- const response = await fetch(`${API_BASE}/api/mcp/connect`, {
957
- method: 'POST',
958
- headers: {
959
- 'Content-Type': 'application/json',
960
- },
961
- body: JSON.stringify(connectionData),
962
- });
963
-
964
- if (response.ok) {
965
- const result = await response.json();
966
- this.currentServer = {
967
- ...connectionData,
968
- isConnected: true,
969
- tools: result.tools || [],
970
- };
971
-
972
- this.showToast(`Reconnected to ${this.currentServer.name}`, 'success');
973
- await this.loadCurrentServer();
974
- this.updateConnectionStatus();
975
- } else {
976
- const errorText = await response.text();
977
- this.showToast(`Reconnection failed: ${errorText}`, 'error');
978
- }
979
- } catch (error) {
980
- console.error('Reconnect error:', error);
981
- this.showToast('Reconnection failed: ' + error.message, 'error');
982
- } finally {
983
- this.hideLoading();
984
- }
985
- }
986
-
987
- updateConnectionStatus () {
988
- if (!this.connectionStatus) {return;}
989
- if (this.currentServer && this.currentServer.isConnected) {
990
- this.connectionStatus.textContent = `Connected to ${this.currentServer.name}`;
991
- this.connectionStatus.classList.add('connected');
992
- } else {
993
- this.connectionStatus.textContent = 'Not Connected';
994
- this.connectionStatus.classList.remove('connected');
995
- }
996
- }
997
-
998
- handleInputChange () {
999
- const length = this.messageInput.value.length;
1000
- this.charCount.textContent = `${length}/40000`;
1001
-
1002
- const isEmpty = this.messageInput.value.trim() === '';
1003
- this.sendButton.disabled = isEmpty;
1004
-
1005
- if (length >= 3800) {
1006
- this.charCount.style.color = '#e74c3c';
1007
- } else if (length >= 3500) {
1008
- this.charCount.style.color = '#f39c12';
1009
- } else {
1010
- this.charCount.style.color = '#95a5a6';
1011
- }
1012
- }
1013
-
1014
- handleKeyDown (event) {
1015
- if (event.key === 'Enter' && !event.shiftKey) {
1016
- event.preventDefault();
1017
- this.sendMessage();
1018
- }
1019
- }
1020
-
1021
- async sendMessage () {
1022
- const message = this.messageInput.value.trim();
1023
- if (!message) {return;}
1024
-
1025
- if (!this.validateCustomModelSettings()) {
1026
- return;
1027
- }
1028
-
1029
- this.addMessage(message, 'user');
1030
-
1031
- this.messageInput.value = '';
1032
- this.handleInputChange();
1033
- this.messageInput.style.height = 'auto';
1034
-
1035
- this.showTypingIndicator();
1036
-
1037
- try {
1038
- const modelConfig = this.getModelConfig();
1039
-
1040
- const requestData = {
1041
- message: message,
1042
- sessionId: this.currentSessionId,
1043
- agentPrompt: trim(this.systemPromptTextarea.value) || undefined,
1044
- customPrompt: trim(this.customPromptTextarea.value) || undefined,
1045
- model: modelConfig.model,
1046
- useStreaming: false,
1047
- mcpConfig: this.mcpConfig.url ? {
1048
- url: this.mcpConfig.url,
1049
- transport: this.mcpConfig.transport,
1050
- headers: this.mcpConfig.headers,
1051
- name: this.mcpConfig.name,
1052
- } : undefined,
1053
- modelConfig: modelConfig,
1054
- };
1055
-
1056
- const response = await fetch(`${API_BASE}/api/chat/message`, {
1057
- method: 'POST',
1058
- headers: {
1059
- 'Content-Type': 'application/json',
1060
- },
1061
- body: JSON.stringify(requestData),
1062
- });
1063
-
1064
- if (!response.ok) {
1065
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1066
- }
1067
-
1068
- const result = await response.json();
1069
-
1070
- this.currentSessionId = result.sessionId;
1071
-
1072
- this.addMessage(result.message, 'assistant', result.metadata);
1073
-
1074
- } catch (error) {
1075
- console.error('Send message error:', error);
1076
- this.addMessage(`Error: ${error.message}`, 'assistant', { error: true });
1077
- this.showToast('Failed to send message: ' + error.message, 'error');
1078
- } finally {
1079
- this.hideTypingIndicator();
1080
- }
1081
- }
1082
-
1083
- addMessage (text, sender, metadata = {}) {
1084
- const messageId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1085
- const messageDiv = document.createElement('div');
1086
- messageDiv.className = `message ${sender}`;
1087
- messageDiv.dataset.messageId = messageId;
1088
-
1089
- if (metadata.error) {
1090
- messageDiv.classList.add('error');
1091
- }
1092
-
1093
- const avatar = document.createElement('div');
1094
- avatar.className = 'message-avatar';
1095
- avatar.innerHTML = sender === 'user' ? '<span class="material-icons-round">person</span>' : '<span class="material-icons-round">smart_toy</span>';
1096
-
1097
- const content = document.createElement('div');
1098
- content.className = 'message-content';
1099
-
1100
- if (sender === 'assistant' && !metadata.error) {
1101
- this.messageFormats[messageId] = this.defaultDisplayFormat;
1102
- this.messageTexts[messageId] = text;
1103
-
1104
- const formatToggle = this.createFormatToggle(messageId);
1105
- content.appendChild(formatToggle);
1106
- }
1107
-
1108
- const messageText = document.createElement('div');
1109
- messageText.className = 'message-text';
1110
- messageText.dataset.messageId = messageId;
1111
-
1112
- if (sender === 'assistant' && !metadata.error) {
1113
- const format = this.messageFormats[messageId];
1114
- this.renderMessageContent(messageText, text, format);
1115
- } else {
1116
- messageText.textContent = text;
1117
- }
1118
-
1119
- const messageTime = document.createElement('div');
1120
- messageTime.className = 'message-time';
1121
- messageTime.textContent = new Date().toLocaleTimeString();
1122
-
1123
- content.appendChild(messageText);
1124
- content.appendChild(messageTime);
1125
-
1126
- if (sender === 'assistant' && metadata && !metadata.error) {
1127
- if (metadata.tools_used && metadata.tools_used.length > 0) {
1128
- const toolsUsed = document.createElement('div');
1129
- toolsUsed.className = 'message-tools';
1130
- toolsUsed.innerHTML = `<small class="a-info">Tools used: ${metadata.tools_used.join(', ')}</small>`;
1131
- content.appendChild(toolsUsed);
1132
- }
1133
-
1134
- if (metadata.response_time) {
1135
- const responseTime = document.createElement('div');
1136
- responseTime.className = 'message-timing';
1137
- responseTime.innerHTML = `<small class="a-info">Response time: ${metadata.response_time}ms</small>`;
1138
- content.appendChild(responseTime);
1139
- }
1140
- }
1141
-
1142
- messageDiv.appendChild(avatar);
1143
- messageDiv.appendChild(content);
1144
-
1145
- if (sender === 'user') {
1146
- messageDiv.addEventListener('dblclick', () => {
1147
- const currentValue = this.messageInput.value;
1148
- const newValue = currentValue ? currentValue + ' ' + text : text;
1149
- this.messageInput.value = newValue;
1150
- this.messageInput.focus();
1151
- this.handleInputChange();
1152
- });
1153
- messageDiv.style.cursor = 'pointer';
1154
- messageDiv.title = 'Double-click to add text to input field';
1155
- }
1156
-
1157
- this.chatMessages.appendChild(messageDiv);
1158
- this.scrollToBottom();
1159
- }
1160
-
1161
- showTypingIndicator () {
1162
- this.typingIndicator.classList.add('visible');
1163
- }
1164
-
1165
- hideTypingIndicator () {
1166
- this.typingIndicator.classList.remove('visible');
1167
- }
1168
-
1169
- clearChat () {
1170
- const welcomeMessage = this.chatMessages.querySelector('.message.welcome');
1171
- this.chatMessages.innerHTML = '';
1172
- if (welcomeMessage) {
1173
- this.chatMessages.appendChild(welcomeMessage);
1174
- }
1175
-
1176
- this.currentSessionId = null;
1177
-
1178
- this.showToast('Chat cleared', 'success');
1179
- }
1180
-
1181
- scrollToBottom () {
1182
- setTimeout(() => {
1183
- this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
1184
- }, 100);
1185
- }
1186
-
1187
- showLoading (message = 'Loading...') {
1188
- this.loadingOverlay.querySelector('span').textContent = message;
1189
- this.loadingOverlay.style.display = 'flex';
1190
- }
1191
-
1192
- hideLoading () {
1193
- this.loadingOverlay.style.display = 'none';
1194
- }
1195
-
1196
- showToast (message, type = 'info') {
1197
- const toast = document.createElement('div');
1198
- toast.className = `toast ${type}`;
1199
-
1200
- const icon = {
1201
- 'success': 'check_circle',
1202
- 'error': 'error',
1203
- 'warning': 'warning',
1204
- 'info': 'info',
1205
- }[type] || 'info';
1206
-
1207
- toast.innerHTML = `
1208
- <span class="material-icons-round">${icon}</span>
1209
- <span>${message}</span>
1210
- `;
1211
-
1212
- this.toastContainer.appendChild(toast);
1213
-
1214
- setTimeout(() => {
1215
- if (toast.parentNode) {
1216
- toast.parentNode.removeChild(toast);
1217
- }
1218
- }, 5000);
1219
-
1220
- toast.addEventListener('click', () => {
1221
- if (toast.parentNode) {
1222
- toast.parentNode.removeChild(toast);
1223
- }
1224
- });
1225
- }
1226
-
1227
- handleModelSelectChange () {
1228
- const isOther = this.modelSelect.value === 'other';
1229
- this.customModelSettings.style.display = isOther ? 'block' : 'none';
1230
- }
1231
-
1232
- validateCustomModelSettings () {
1233
- if (this.modelSelect.value !== 'other') {
1234
- return true;
1235
- }
1236
-
1237
- const baseURL = trim(this.customBaseUrl.value);
1238
- const apiKey = trim(this.customApiKey.value);
1239
- const modelName = trim(this.customModelName.value);
1240
- const temperature = this.modelTemperature.value;
1241
- const maxTokens = this.modelMaxTokens.value;
1242
-
1243
- const missingFields = [];
1244
- if (!baseURL) {missingFields.push('Base URL');}
1245
- if (!apiKey) {missingFields.push('API Key');}
1246
- if (!modelName) {missingFields.push('Model Name');}
1247
- if (!temperature) {missingFields.push('Temperature');}
1248
- if (!maxTokens) {missingFields.push('Max Tokens');}
1249
-
1250
- if (missingFields.length > 0) {
1251
- this.showToast(`Missing required fields: ${missingFields.join(', ')}`, 'error');
1252
- return false;
1253
- }
1254
-
1255
- return true;
1256
- }
1257
-
1258
- getModelConfig () {
1259
- const isOther = this.modelSelect.value === 'other';
1260
- const t = parseFloat(this.modelTemperature.value);
1261
- const temperature = Number.isNaN(t) ? 0.1 : t;
1262
- const maxTokens = parseInt(this.modelMaxTokens.value, 10) || 2048;
1263
- const maxTurns = parseInt(this.modelMaxTurns.value, 10) || 10;
1264
- const toolResultLimitChars = parseInt(this.toolResultLimitChars.value, 10) || 20000;
1265
-
1266
- if (isOther) {
1267
- return {
1268
- baseURL: trim(this.customBaseUrl.value),
1269
- apiKey: trim(this.customApiKey.value),
1270
- model: trim(this.customModelName.value),
1271
- temperature: temperature,
1272
- maxTokens: maxTokens,
1273
- maxTurns: maxTurns,
1274
- toolResultLimitChars: toolResultLimitChars,
1275
- };
1276
- }
1277
-
1278
- return {
1279
- model: this.modelSelect.value,
1280
- temperature: temperature,
1281
- maxTokens: maxTokens,
1282
- maxTurns: maxTurns,
1283
- toolResultLimitChars: toolResultLimitChars,
1284
- };
1285
- }
1286
-
1287
- handleServerUrlChange () {
1288
- this.stopAuthRefresh();
1289
- let url = this.serverUrlInput.value.trim();
1290
-
1291
- if (url) {
1292
- url = url.replace(/\/+$/, '');
1293
-
1294
- try {
1295
- const parsedUrl = new URL(url);
1296
- if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
1297
- url = url + '/mcp';
1298
- }
1299
- } catch {
1300
- if (!url.includes('/')) {
1301
- url = url + '/mcp';
1302
- } else if (url.endsWith('/')) {
1303
- url = url + 'mcp';
1304
- } else if (!url.split('/').slice(1).join('/')) {
1305
- url = url + '/mcp';
1306
- }
1307
- }
1308
-
1309
- this.serverUrlInput.value = url;
1310
- }
1311
-
1312
- this.saveFormValuesToStorage();
1313
- }
1314
-
1315
- saveFormValuesToStorage () {
1316
- const formData = {
1317
- serverUrl: this.serverUrlInput.value,
1318
- transport: this.transportSelect.value,
1319
- model: this.modelSelect.value,
1320
- agentPrompt: trim(this.systemPromptTextarea.value),
1321
- customPrompt: trim(this.customPromptTextarea.value),
1322
- customBaseUrl: trim(this.customBaseUrl.value),
1323
- customApiKey: trim(this.customApiKey.value),
1324
- customModelName: trim(this.customModelName.value),
1325
- modelTemperature: this.modelTemperature.value,
1326
- modelMaxTokens: this.modelMaxTokens.value,
1327
- modelMaxTurns: this.modelMaxTurns.value,
1328
- toolResultLimitChars: this.toolResultLimitChars.value,
1329
- };
1330
- localStorage.setItem('mcpAgentFormValues', JSON.stringify(formData));
1331
- }
1332
-
1333
- loadFormValuesFromURL () {
1334
- try {
1335
- const params = new URLSearchParams(window.location.search);
1336
- const serverUrl = params.get('serverUrl');
1337
- const transport = params.get('transport');
1338
-
1339
- if (serverUrl) {
1340
- this.serverUrlInput.value = serverUrl;
1341
- }
1342
- if (transport) {
1343
- this.transportSelect.value = transport;
1344
- }
1345
- } catch (error) {
1346
- console.error('Error loading form values from URL:', error);
1347
- }
1348
- }
1349
-
1350
- loadFormValuesFromStorage () {
1351
- try {
1352
- const stored = localStorage.getItem('mcpAgentFormValues');
1353
- if (stored) {
1354
- const formData = JSON.parse(stored);
1355
- if (formData.serverUrl) {this.serverUrlInput.value = formData.serverUrl;}
1356
- if (formData.transport) {this.transportSelect.value = formData.transport;}
1357
- if (formData.model) {this.modelSelect.value = formData.model;}
1358
- if (formData.agentPrompt) {this.systemPromptTextarea.value = trim(formData.agentPrompt);}
1359
- if (formData.customPrompt) {this.customPromptTextarea.value = trim(formData.customPrompt);}
1360
- if (formData.customBaseUrl) {this.customBaseUrl.value = formData.customBaseUrl;}
1361
- if (formData.customApiKey) {this.customApiKey.value = formData.customApiKey;}
1362
- if (formData.customModelName) {this.customModelName.value = formData.customModelName;}
1363
- if (formData.modelTemperature) {this.modelTemperature.value = formData.modelTemperature;}
1364
- if (formData.modelMaxTokens) {this.modelMaxTokens.value = formData.modelMaxTokens;}
1365
- if (formData.modelMaxTurns) {this.modelMaxTurns.value = formData.modelMaxTurns;}
1366
- if (formData.toolResultLimitChars) {this.toolResultLimitChars.value = formData.toolResultLimitChars;}
1367
- this.handleModelSelectChange();
1368
- }
1369
- } catch (error) {
1370
- console.error('Error loading form values from storage:', error);
1371
- }
1372
- }
1373
-
1374
- getSavedUrls () {
1375
- try {
1376
- const saved = localStorage.getItem('mcpSavedUrls');
1377
- return saved ? JSON.parse(saved) : [];
1378
- } catch (error) {
1379
- console.error('Error loading saved URLs:', error);
1380
- return [];
1381
- }
1382
- }
1383
-
1384
- saveSavedUrls (urls) {
1385
- try {
1386
- localStorage.setItem('mcpSavedUrls', JSON.stringify(urls));
1387
- } catch (error) {
1388
- console.error('Error saving URLs:', error);
1389
- }
1390
- }
1391
-
1392
- addUrlToSaved (url) {
1393
- if (!url || url.trim() === '') {
1394
- return;
1395
- }
1396
-
1397
- url = url.trim();
1398
- let savedUrls = this.getSavedUrls();
1399
-
1400
- savedUrls = savedUrls.filter(savedUrl => savedUrl !== url);
1401
-
1402
- savedUrls.unshift(url);
1403
-
1404
- savedUrls = savedUrls.slice(0, 10);
1405
-
1406
- this.saveSavedUrls(savedUrls);
1407
- this.renderSavedUrls();
1408
- }
1409
-
1410
- removeUrlFromSaved (url) {
1411
- let savedUrls = this.getSavedUrls();
1412
- savedUrls = savedUrls.filter(savedUrl => savedUrl !== url);
1413
- this.saveSavedUrls(savedUrls);
1414
- this.renderSavedUrls();
1415
- }
1416
-
1417
- renderSavedUrls () {
1418
- const savedUrls = this.getSavedUrls();
1419
- this.savedUrlsList.innerHTML = '';
1420
-
1421
- if (savedUrls.length === 0) {
1422
- const emptyItem = document.createElement('div');
1423
- emptyItem.className = 'dropdown-item disabled';
1424
- emptyItem.innerHTML = '<span style="color: rgba(255,255,255,0.5);">No saved URLs</span>';
1425
- this.savedUrlsList.appendChild(emptyItem);
1426
- return;
1427
- }
1428
-
1429
- savedUrls.forEach(url => {
1430
- const item = document.createElement('div');
1431
- item.className = 'dropdown-item';
1432
-
1433
- item.innerHTML = `
1434
- <div class="url-item">
1435
- <span class="url-text" title="${url}">${url}</span>
1436
- <button class="delete-btn" title="Delete URL">
1437
- <span class="material-icons-round" style="font-size: 16px;">close</span>
1438
- </button>
1439
- </div>
1440
- `;
1441
-
1442
- item.querySelector('.url-text').addEventListener('click', () => {
1443
- this.selectUrl(url);
1444
- });
1445
-
1446
- item.querySelector('.delete-btn').addEventListener('click', (e) => {
1447
- e.stopPropagation();
1448
- this.removeUrlFromSaved(url);
1449
- });
1450
-
1451
- this.savedUrlsList.appendChild(item);
1452
- });
1453
- }
1454
-
1455
- selectUrl (url) {
1456
- this.serverUrlInput.value = url;
1457
- this.handleServerUrlChange();
1458
- this.closeUrlDropdown();
1459
- this.autoConnect();
1460
- }
1461
-
1462
- toggleUrlDropdown (e) {
1463
- e.preventDefault();
1464
- e.stopPropagation();
1465
-
1466
- const isVisible = this.serverUrlDropdownList.style.display !== 'none';
1467
-
1468
- if (isVisible) {
1469
- this.closeUrlDropdown();
1470
- } else {
1471
- this.openUrlDropdown();
1472
- }
1473
- }
1474
-
1475
- openUrlDropdown () {
1476
- this.renderSavedUrls();
1477
- this.serverUrlDropdownList.style.display = 'block';
1478
- this.serverUrlDropdown.classList.add('active');
1479
-
1480
- const addNewItem = this.serverUrlDropdownList.querySelector('.add-new');
1481
- if (addNewItem) {
1482
- addNewItem.addEventListener('click', () => {
1483
- this.addCurrentUrlToSaved();
1484
- });
1485
- }
1486
- }
1487
-
1488
- closeUrlDropdown () {
1489
- this.serverUrlDropdownList.style.display = 'none';
1490
- this.serverUrlDropdown.classList.remove('active');
1491
- }
1492
-
1493
- addCurrentUrlToSaved () {
1494
- const currentUrl = this.serverUrlInput.value.trim();
1495
- if (currentUrl) {
1496
- this.addUrlToSaved(currentUrl);
1497
- this.closeUrlDropdown();
1498
- this.showToast('URL added to saved', 'success');
1499
- }
1500
- }
1501
-
1502
- handleClickOutside (e) {
1503
- const container = e.target.closest('.custom-select-container');
1504
- if (!container) {
1505
- this.closeUrlDropdown();
1506
- }
1507
- }
1508
- }
1509
-
1510
- // Initialize the app when DOM is loaded
1511
- document.addEventListener('DOMContentLoaded', () => {
1512
- window.mcpAgentTester = new McpAgentTester();
1513
- });
1
+ const API_BASE = '/agent-tester';
2
+ const trim = (s) => String(s || '').trim();
3
+
4
+ class McpAgentTester {
5
+ constructor () {
6
+ this.currentSessionId = null;
7
+ this.currentServer = null;
8
+ this.currentSystemPrompt = '';
9
+ this.usedHeaders = [];
10
+ this.pendingConnectionData = null;
11
+ this._headersUpdateTimer = null;
12
+ this.defaultMcpUrl = null;
13
+ this.authEnabled = false;
14
+ this.configHttpHeaders = {};
15
+ this._authRefreshInterval = null;
16
+ this._currentAuthType = null;
17
+ this.messageFormats = {};
18
+ this.messageTexts = {};
19
+ this.defaultDisplayFormat = localStorage.getItem('agentTesterDefaultFormat') || 'HTML';
20
+
21
+ this.mcpConfig = {
22
+ url: null,
23
+ transport: 'http',
24
+ headers: {},
25
+ name: null,
26
+ };
27
+
28
+ this.initializeElements();
29
+ this.initTheme();
30
+ this.bindEvents();
31
+ this.loadInitialData();
32
+
33
+ this.setupAutoResize();
34
+
35
+ console.log('MCP Agent Tester initialized');
36
+ }
37
+
38
+ sanitizeHtml (html) {
39
+ const allowedTags = [
40
+ 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'code', 'pre',
41
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
42
+ 'ul', 'ol', 'li', 'blockquote', 'a', 'span', 'div',
43
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
44
+ ];
45
+
46
+ const allowedAttributes = {
47
+ 'a': ['href', 'title', 'target'],
48
+ 'th': ['colspan', 'rowspan'],
49
+ 'td': ['colspan', 'rowspan'],
50
+ 'code': ['class'],
51
+ 'pre': ['class'],
52
+ 'span': ['class'],
53
+ 'div': ['class'],
54
+ };
55
+
56
+ const tempDiv = document.createElement('div');
57
+ tempDiv.innerHTML = html;
58
+
59
+ const cleanNode = (node) => {
60
+ if (node.nodeType === Node.TEXT_NODE) {
61
+ return node;
62
+ }
63
+
64
+ if (node.nodeType !== Node.ELEMENT_NODE) {
65
+ return null;
66
+ }
67
+
68
+ const tagName = node.tagName.toLowerCase();
69
+
70
+ if (!allowedTags.includes(tagName)) {
71
+ const textNode = document.createTextNode(node.textContent || '');
72
+ return textNode;
73
+ }
74
+
75
+ const cleanedElement = document.createElement(tagName);
76
+
77
+ const allowedAttrs = allowedAttributes[tagName] || [];
78
+ allowedAttrs.forEach(attr => {
79
+ if (node.hasAttribute(attr)) {
80
+ const value = node.getAttribute(attr);
81
+ if (attr === 'href') {
82
+ try {
83
+ const url = new URL(value, window.location.href);
84
+ if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
85
+ cleanedElement.setAttribute(attr, value);
86
+ }
87
+ } catch {
88
+ // Invalid URL, skip
89
+ }
90
+ } else {
91
+ cleanedElement.setAttribute(attr, value);
92
+ }
93
+ }
94
+ });
95
+
96
+ Array.from(node.childNodes).forEach(child => {
97
+ const cleanedChild = cleanNode(child);
98
+ if (cleanedChild) {
99
+ cleanedElement.appendChild(cleanedChild);
100
+ }
101
+ });
102
+
103
+ return cleanedElement;
104
+ };
105
+
106
+ const cleanedNodes = Array.from(tempDiv.childNodes).map(cleanNode).filter(node => node !== null);
107
+
108
+ const finalDiv = document.createElement('div');
109
+ cleanedNodes.forEach(node => finalDiv.appendChild(node));
110
+
111
+ return finalDiv.innerHTML.trim();
112
+ }
113
+
114
+ createFormatToggle (messageId) {
115
+ const toggleContainer = document.createElement('div');
116
+ toggleContainer.className = 'format-toggle-container';
117
+
118
+ const select = document.createElement('select');
119
+ select.className = 'format-toggle';
120
+ select.dataset.messageId = messageId;
121
+
122
+ const options = ['MD', 'HTML'];
123
+ const currentFormat = this.messageFormats[messageId] || 'MD';
124
+
125
+ options.forEach(opt => {
126
+ const option = document.createElement('option');
127
+ option.value = opt;
128
+ option.textContent = opt;
129
+ if (opt === currentFormat) {
130
+ option.selected = true;
131
+ }
132
+ select.appendChild(option);
133
+ });
134
+
135
+ select.addEventListener('change', (e) => {
136
+ this.onFormatChange(messageId, e.target.value);
137
+ });
138
+
139
+ toggleContainer.appendChild(select);
140
+ return toggleContainer;
141
+ }
142
+
143
+ onFormatChange (messageId, format) {
144
+ this.messageFormats[messageId] = format;
145
+ const originalText = this.messageTexts[messageId];
146
+ const messageText = document.querySelector(`.message-text[data-message-id="${messageId}"]`);
147
+ if (messageText && originalText) {
148
+ this.renderMessageContent(messageText, originalText, format);
149
+ }
150
+ }
151
+
152
+ handleDefaultFormatChange () {
153
+ const { value } = this.defaultFormatSelect;
154
+ this.defaultDisplayFormat = value;
155
+ localStorage.setItem('agentTesterDefaultFormat', value);
156
+ if (value === 'HTML') {
157
+ this.showToast('Tip: add "Format your response in HTML" to Custom Prompt for best results', 'info');
158
+ }
159
+ }
160
+
161
+ renderMessageContent (element, text, format) {
162
+ if (format === 'HTML') {
163
+ element.innerHTML = this.sanitizeHtml(text).trim();
164
+ element.classList.add('html-content');
165
+ element.classList.remove('md-content');
166
+ } else {
167
+ element.textContent = text;
168
+ element.classList.add('md-content');
169
+ element.classList.remove('html-content');
170
+ }
171
+ }
172
+
173
+ initializeElements () {
174
+ this.sidebar = document.getElementById('sidebar');
175
+ this.sidebarToggle = document.getElementById('sidebarToggle');
176
+ this.sidebarToggleMobile = document.getElementById('sidebarToggleMobile');
177
+
178
+ this.mcpConnectionForm = document.getElementById('mcpConnectionForm');
179
+ this.serverUrlInput = document.getElementById('serverUrl');
180
+ this.transportSelect = document.getElementById('transport');
181
+
182
+ this.serverUrlDropdown = document.getElementById('serverUrlDropdown');
183
+ this.serverUrlDropdownList = document.getElementById('serverUrlDropdownList');
184
+ this.savedUrlsList = document.getElementById('savedUrlsList');
185
+
186
+ this.currentServer = null;
187
+
188
+ this.headersSection = document.getElementById('headersSection');
189
+ this.dynamicHeaders = document.getElementById('dynamicHeaders');
190
+
191
+ this.modelSelect = document.getElementById('modelSelect');
192
+
193
+ this.customModelSettings = document.getElementById('customModelSettings');
194
+ this.customBaseUrl = document.getElementById('customBaseUrl');
195
+ this.customApiKey = document.getElementById('customApiKey');
196
+ this.customModelName = document.getElementById('customModelName');
197
+ this.modelTemperature = document.getElementById('modelTemperature');
198
+ this.modelMaxTokens = document.getElementById('modelMaxTokens');
199
+ this.modelMaxTurns = document.getElementById('modelMaxTurns');
200
+ this.toolResultLimitChars = document.getElementById('toolResultLimitChars');
201
+
202
+ this.systemPromptTextarea = document.getElementById('systemPrompt');
203
+ this.customPromptTextarea = document.getElementById('customPrompt');
204
+
205
+ this.connectedServersContainer = document.getElementById('connectedServers');
206
+
207
+ this.chatMessages = document.getElementById('chatMessages');
208
+ this.messageInput = document.getElementById('messageInput');
209
+ this.sendButton = document.getElementById('sendButton');
210
+ this.clearChatBtn = document.getElementById('clearChat');
211
+ this.connectionStatus = document.getElementById('connectionStatus');
212
+ this.charCount = document.getElementById('charCount');
213
+ this.typingIndicator = document.getElementById('typingIndicator');
214
+
215
+ this.loadingOverlay = document.getElementById('loadingOverlay');
216
+ this.toastContainer = document.getElementById('toastContainer');
217
+
218
+ this.themeToggle = document.getElementById('themeToggle');
219
+ this.defaultFormatSelect = document.getElementById('defaultDisplayFormat');
220
+ }
221
+
222
+ bindEvents () {
223
+ if (this.sidebarToggle) {
224
+ this.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
225
+ }
226
+ if (this.sidebarToggleMobile) {
227
+ this.sidebarToggleMobile.addEventListener('click', () => this.toggleSidebar());
228
+ }
229
+
230
+ if (this.themeToggle) {
231
+ this.themeToggle.addEventListener('click', () => this.toggleTheme());
232
+ }
233
+
234
+ if (this.defaultFormatSelect) {
235
+ this.defaultFormatSelect.value = this.defaultDisplayFormat;
236
+ this.defaultFormatSelect.addEventListener('change', () => this.handleDefaultFormatChange());
237
+ }
238
+
239
+ this.mcpConnectionForm.addEventListener('submit', (e) => this.handleMcpConnection(e));
240
+
241
+ this.serverUrlInput.addEventListener('input', () => this.handleServerUrlChange());
242
+ this.transportSelect.addEventListener('change', () => this.saveFormValuesToStorage());
243
+
244
+ this.serverUrlDropdown.addEventListener('click', (e) => this.toggleUrlDropdown(e));
245
+ document.addEventListener('click', (e) => this.handleClickOutside(e));
246
+
247
+ this.modelSelect.addEventListener('change', () => {
248
+ this.handleModelSelectChange();
249
+ this.saveFormValuesToStorage();
250
+ });
251
+ this.systemPromptTextarea.addEventListener('input', () => this.saveFormValuesToStorage());
252
+ this.customPromptTextarea.addEventListener('input', () => this.saveFormValuesToStorage());
253
+
254
+ this.customBaseUrl.addEventListener('input', () => this.saveFormValuesToStorage());
255
+ this.customApiKey.addEventListener('input', () => this.saveFormValuesToStorage());
256
+ this.customModelName.addEventListener('input', () => this.saveFormValuesToStorage());
257
+ this.modelTemperature.addEventListener('input', () => this.saveFormValuesToStorage());
258
+ this.modelMaxTokens.addEventListener('input', () => this.saveFormValuesToStorage());
259
+ this.modelMaxTurns.addEventListener('input', () => this.saveFormValuesToStorage());
260
+ this.toolResultLimitChars.addEventListener('input', () => this.saveFormValuesToStorage());
261
+
262
+ document.querySelectorAll('.btn-enlarge').forEach(btn => {
263
+ btn.addEventListener('click', () => this.openPromptModal(btn.dataset.target));
264
+ });
265
+ document.getElementById('promptModalClose').addEventListener('click', () => this.closePromptModal());
266
+ document.getElementById('promptModalSave').addEventListener('click', () => this.savePromptModal());
267
+ document.getElementById('promptModal').addEventListener('click', (e) => {
268
+ if (e.target === e.currentTarget) {this.closePromptModal();}
269
+ });
270
+
271
+ this.messageInput.addEventListener('input', () => this.handleInputChange());
272
+ this.messageInput.addEventListener('keydown', (e) => this.handleKeyDown(e));
273
+ this.sendButton.addEventListener('click', () => this.sendMessage());
274
+ this.clearChatBtn.addEventListener('click', () => this.clearChat());
275
+
276
+ document.addEventListener('click', (e) => {
277
+ if (window.innerWidth <= 768 &&
278
+ !this.sidebar.contains(e.target) &&
279
+ !this.sidebarToggleMobile.contains(e.target) &&
280
+ this.sidebar.classList.contains('open')) {
281
+ this.toggleSidebar();
282
+ }
283
+ });
284
+
285
+ window.addEventListener('resize', () => {
286
+ if (window.innerWidth > 768) {
287
+ this.sidebar.classList.remove('open');
288
+ }
289
+ });
290
+ }
291
+
292
+ initTheme () {
293
+ const saved = localStorage.getItem('mcpAgentTheme');
294
+ let theme = saved;
295
+ if (!theme) {
296
+ theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
297
+ ? 'dark' : 'light';
298
+ }
299
+ this.applyTheme(theme);
300
+ }
301
+
302
+ toggleTheme () {
303
+ const current = document.documentElement.getAttribute('data-theme') || 'light';
304
+ const next = current === 'dark' ? 'light' : 'dark';
305
+ this.applyTheme(next);
306
+ localStorage.setItem('mcpAgentTheme', next);
307
+ }
308
+
309
+ applyTheme (theme) {
310
+ document.documentElement.setAttribute('data-theme', theme);
311
+ if (this.themeToggle) {
312
+ const icon = this.themeToggle.querySelector('.material-icons-round');
313
+ if (icon) {
314
+ icon.textContent = theme === 'dark' ? 'light_mode' : 'dark_mode';
315
+ }
316
+ }
317
+ }
318
+
319
+ openPromptModal (targetId) {
320
+ this._promptModalTarget = document.getElementById(targetId);
321
+ const modal = document.getElementById('promptModal');
322
+ const textarea = document.getElementById('promptModalTextarea');
323
+ const title = document.getElementById('promptModalTitle');
324
+ title.textContent = targetId === 'systemPrompt' ? 'Agent Prompt' : 'Custom Prompt';
325
+ textarea.value = this._promptModalTarget.value;
326
+ modal.style.display = 'flex';
327
+ textarea.focus();
328
+ }
329
+
330
+ closePromptModal () {
331
+ document.getElementById('promptModal').style.display = 'none';
332
+ this._promptModalTarget = null;
333
+ }
334
+
335
+ savePromptModal () {
336
+ if (this._promptModalTarget) {
337
+ this._promptModalTarget.value = document.getElementById('promptModalTextarea').value;
338
+ this.saveFormValuesToStorage();
339
+ }
340
+ this.closePromptModal();
341
+ }
342
+
343
+ setupAutoResize () {
344
+ this.messageInput.addEventListener('input', function () {
345
+ this.style.height = 'auto';
346
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
347
+ });
348
+ }
349
+
350
+ toggleSidebar () {
351
+ this.sidebar.classList.toggle('open');
352
+ }
353
+
354
+ async loadInitialData () {
355
+ try {
356
+ this.loadFormValuesFromStorage();
357
+ this.loadFormValuesFromURL();
358
+ this.handleServerUrlChange();
359
+ this.renderSavedUrls();
360
+ await this.loadDefaultConfig();
361
+ await this.loadCurrentServer();
362
+ this.currentSystemPrompt = this.systemPromptTextarea.value;
363
+
364
+ const serverUrl = this.serverUrlInput.value.trim();
365
+ if (serverUrl && (!this.currentServer || !this.currentServer.isConnected)) {
366
+ // Auto-connect if there's a URL but no connected server
367
+ await this.autoConnect();
368
+ } else if (serverUrl && this.currentServer && this.currentServer.isConnected) {
369
+ // Already connected — still need to load headers
370
+ await this.checkRequiredHeaders();
371
+ }
372
+ } catch (error) {
373
+ console.error('Error loading initial data:', error);
374
+ }
375
+ }
376
+
377
+ async autoConnect () {
378
+ const serverUrl = this.serverUrlInput.value.trim();
379
+ if (!serverUrl) {return;}
380
+
381
+ const transport = this.transportSelect.value;
382
+ const serverName = this.generateServerName(serverUrl);
383
+
384
+ const connectionData = {
385
+ name: serverName,
386
+ url: serverUrl,
387
+ transport: transport,
388
+ headers: this.getHeadersFromForm(),
389
+ };
390
+
391
+ this.showLoading('Auto-connecting to MCP server...');
392
+
393
+ try {
394
+ const response = await fetch(`${API_BASE}/api/mcp/connect`, {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/json' },
397
+ body: JSON.stringify(connectionData),
398
+ });
399
+
400
+ const result = await response.json();
401
+
402
+ if (result.success) {
403
+ this.currentServer = {
404
+ name: serverName,
405
+ url: serverUrl,
406
+ isConnected: true,
407
+ ...result.config,
408
+ };
409
+
410
+ this.mcpConfig = {
411
+ url: serverUrl,
412
+ transport: transport,
413
+ headers: this.getHeadersFromForm(),
414
+ name: serverName,
415
+ };
416
+
417
+ if (result.config && result.config.agentPrompt) {
418
+ this.systemPromptTextarea.value = result.config.agentPrompt;
419
+ this.currentSystemPrompt = result.config.agentPrompt;
420
+ }
421
+
422
+ this.addUrlToSaved(serverUrl);
423
+ this.headersSection.style.display = 'none';
424
+ this.dynamicHeaders.innerHTML = '';
425
+ this.usedHeaders = [];
426
+ this.updateConnectionStatus();
427
+ this.renderServerInfo();
428
+
429
+ await this.checkRequiredHeaders();
430
+
431
+ this.showToast('Auto-connected to ' + serverName, 'success');
432
+ } else {
433
+ console.warn('Auto-connect failed:', result.error);
434
+ }
435
+ } catch (error) {
436
+ console.warn('Auto-connect failed:', error.message);
437
+ } finally {
438
+ this.hideLoading();
439
+ }
440
+ }
441
+
442
+ async loadDefaultConfig () {
443
+ try {
444
+ const response = await fetch(`${API_BASE}/api/config`);
445
+ const config = await response.json();
446
+ this.defaultMcpUrl = config.defaultMcpUrl || null;
447
+ this.authEnabled = !!config.authEnabled;
448
+ this.configHttpHeaders = config.httpHeaders || {};
449
+ if (config.defaultMcpUrl) {
450
+ const serverUrlInput = document.getElementById('serverUrl');
451
+ if (!this.mcpConfig.url && !serverUrlInput.value) {
452
+ serverUrlInput.value = config.defaultMcpUrl;
453
+ }
454
+ }
455
+ } catch (e) {
456
+ console.warn('Failed to load default config:', e);
457
+ }
458
+ }
459
+
460
+ async handleMcpConnection (event) {
461
+ event.preventDefault();
462
+
463
+ const serverUrl = this.serverUrlInput.value.trim();
464
+ const transport = this.transportSelect.value;
465
+
466
+ const serverName = this.generateServerName(serverUrl);
467
+
468
+ const connectionData = {
469
+ name: serverName,
470
+ url: serverUrl,
471
+ transport: transport,
472
+ headers: this.getHeadersFromForm(),
473
+ };
474
+
475
+ this.showLoading('Connecting to MCP server...');
476
+
477
+ try {
478
+ const response = await fetch(`${API_BASE}/api/mcp/connect`, {
479
+ method: 'POST',
480
+ headers: { 'Content-Type': 'application/json' },
481
+ body: JSON.stringify(connectionData),
482
+ });
483
+
484
+ const result = await response.json();
485
+
486
+ if (result.success) {
487
+ this.currentServer = {
488
+ name: serverName,
489
+ url: serverUrl,
490
+ isConnected: true,
491
+ ...result.config,
492
+ };
493
+
494
+ this.mcpConfig = {
495
+ url: serverUrl,
496
+ transport: transport,
497
+ headers: this.getHeadersFromForm(),
498
+ name: serverName,
499
+ };
500
+
501
+ if (result.config && result.config.agentPrompt) {
502
+ this.systemPromptTextarea.value = result.config.agentPrompt;
503
+ this.currentSystemPrompt = result.config.agentPrompt;
504
+ }
505
+
506
+ this.showToast('Successfully connected to ' + serverName, 'success');
507
+
508
+ this.addUrlToSaved(serverUrl);
509
+
510
+ this.headersSection.style.display = 'none';
511
+ this.dynamicHeaders.innerHTML = '';
512
+ this.usedHeaders = [];
513
+
514
+ this.updateConnectionStatus();
515
+ this.renderServerInfo();
516
+
517
+ await this.checkRequiredHeaders();
518
+ } else {
519
+ this.showToast('Failed to connect: ' + result.error, 'error');
520
+ }
521
+
522
+ } catch (error) {
523
+ console.error('Connection error:', error);
524
+ this.showToast('Connection failed: ' + error.message, 'error');
525
+ } finally {
526
+ this.hideLoading();
527
+ }
528
+ }
529
+
530
+ generateServerName (url) {
531
+ try {
532
+ const parsedUrl = new URL(url);
533
+ const { hostname, port } = parsedUrl;
534
+
535
+ let serverName = hostname;
536
+ serverName = serverName.replace(/^www\./, '').split('.')[0] || 'MCP Server';
537
+
538
+ if (port && port !== '80' && port !== '443') {
539
+ serverName += ':' + port;
540
+ }
541
+ return serverName;
542
+ } catch {
543
+ return url.split('/')[2] || 'MCP Server';
544
+ }
545
+ }
546
+
547
+ async checkRequiredHeaders () {
548
+ const url = this.serverUrlInput.value.trim();
549
+
550
+ if (!url) {
551
+ this.showToast('Please enter a server URL first', 'warning');
552
+ return;
553
+ }
554
+
555
+ this.showLoading('Checking used headers...');
556
+
557
+ try {
558
+ const response = await fetch(`${API_BASE}/api/mcp/used-headers?url=${encodeURIComponent(url)}`, {
559
+ method: 'GET',
560
+ headers: { 'Accept': 'application/json' },
561
+ });
562
+
563
+ if (response.ok) {
564
+ const headers = await response.json();
565
+ this.usedHeaders = Array.isArray(headers) ? headers : [];
566
+ this.renderHeaderInputs();
567
+ await this.autoFillAuthHeader();
568
+
569
+ if (this.usedHeaders.length > 0) {
570
+ const reqCount = this.usedHeaders.filter(h => !h.isOptional).length;
571
+ this.showToast(`Found ${this.usedHeaders.length} headers (${reqCount} used)`, 'success');
572
+ this.headersSection.style.display = 'block';
573
+ } else {
574
+ this.showToast('No additional headers used', 'info');
575
+ this.headersSection.style.display = 'none';
576
+ }
577
+ } else {
578
+ this.showToast('Headers endpoint not available - proceeding without additional headers', 'info');
579
+ this.headersSection.style.display = 'none';
580
+ this.usedHeaders = [];
581
+ }
582
+
583
+ } catch (error) {
584
+ console.log('Headers check failed:', error);
585
+ this.showToast('Headers endpoint not available - proceeding without additional headers', 'info');
586
+ this.headersSection.style.display = 'none';
587
+ this.usedHeaders = [];
588
+ } finally {
589
+ this.hideLoading();
590
+ }
591
+ }
592
+
593
+ renderHeaderInputs () {
594
+ this.dynamicHeaders.innerHTML = '';
595
+ const savedHeaders = this.loadHeaderValuesFromStorage();
596
+
597
+ this.usedHeaders.forEach(header => {
598
+ const headerGroup = document.createElement('div');
599
+ headerGroup.className = 'header-row';
600
+
601
+ const savedValue = savedHeaders[header.name] || this.configHttpHeaders[header.name] || '';
602
+ const isRequired = !header.isOptional;
603
+ const hasDesc = header.description && header.description.trim();
604
+ const nameClass = hasDesc ? 'header-name has-tooltip' : 'header-name';
605
+ const tooltipAttr = hasDesc ? ` data-tooltip="${header.description.replace(/"/g, '&quot;')}"` : '';
606
+ const inputClass = isRequired ? 'header-value used-header' : 'header-value';
607
+
608
+ headerGroup.innerHTML = `
609
+ <span class="${nameClass}"${tooltipAttr}>${header.name}</span>
610
+ <input
611
+ type="text"
612
+ class="${inputClass}"
613
+ id="header_${header.name}"
614
+ placeholder="${header.name}"
615
+ data-header-name="${header.name}"
616
+ data-required="${isRequired}"
617
+ value="${savedValue.replace(/"/g, '&quot;')}"
618
+ >
619
+ `;
620
+
621
+ this.dynamicHeaders.appendChild(headerGroup);
622
+
623
+ const nameEl = headerGroup.querySelector('.header-name');
624
+ if (nameEl && hasDesc) {
625
+ nameEl.style.cursor = 'pointer';
626
+ nameEl.addEventListener('click', (e) => {
627
+ e.stopPropagation();
628
+ this.toggleHeaderTooltip(e, header.description);
629
+ });
630
+ }
631
+
632
+ const inputEl = headerGroup.querySelector(`#header_${header.name}`);
633
+ if (inputEl) {
634
+ inputEl.addEventListener('input', () => {
635
+ this.saveHeaderValuesToStorage();
636
+ this.scheduleHeadersUpdate();
637
+ this.updateHeaderBorder(inputEl);
638
+ });
639
+ this.updateHeaderBorder(inputEl);
640
+ }
641
+ });
642
+
643
+ this.mcpConfig.headers = this.getHeadersFromForm();
644
+ }
645
+
646
+ toggleHeaderTooltip (e, text) {
647
+ const tip = document.getElementById('headerTooltip');
648
+ if (tip.classList.contains('visible') && tip._sourceEl === e.target) {
649
+ this.hideHeaderTooltip();
650
+ return;
651
+ }
652
+ tip._sourceEl = e.target;
653
+ tip.textContent = text;
654
+ const rect = e.target.getBoundingClientRect();
655
+ tip.style.left = rect.left + 'px';
656
+ tip.style.top = (rect.top - 4) + 'px';
657
+ tip.style.transform = 'translateY(-100%)';
658
+ tip.classList.add('visible');
659
+
660
+ const dismissOnClick = (ev) => {
661
+ if (ev.target !== e.target && !tip.contains(ev.target)) {
662
+ this.hideHeaderTooltip();
663
+ document.removeEventListener('click', dismissOnClick);
664
+ }
665
+ };
666
+ setTimeout(() => document.addEventListener('click', dismissOnClick), 0);
667
+ }
668
+
669
+ hideHeaderTooltip () {
670
+ const tip = document.getElementById('headerTooltip');
671
+ tip.classList.remove('visible');
672
+ tip._sourceEl = null;
673
+ }
674
+
675
+ updateHeaderBorder (inputEl) {
676
+ if (inputEl.dataset.required === 'true') {
677
+ if (inputEl.value.trim()) {
678
+ inputEl.classList.remove('empty-required');
679
+ } else {
680
+ inputEl.classList.add('empty-required');
681
+ }
682
+ }
683
+ }
684
+
685
+ getHeaderStorageKey () {
686
+ const url = this.serverUrlInput.value.trim();
687
+ return `mcpHeaderValues_${url}`;
688
+ }
689
+
690
+ saveHeaderValuesToStorage () {
691
+ const headers = this.getHeadersFromForm();
692
+ const key = this.getHeaderStorageKey();
693
+ try {
694
+ localStorage.setItem(key, JSON.stringify(headers));
695
+ } catch (error) {
696
+ console.error('Error saving header values to storage:', error);
697
+ }
698
+ }
699
+
700
+ loadHeaderValuesFromStorage () {
701
+ const key = this.getHeaderStorageKey();
702
+ try {
703
+ const stored = localStorage.getItem(key);
704
+ return stored ? JSON.parse(stored) : {};
705
+ } catch (error) {
706
+ console.error('Error loading header values from storage:', error);
707
+ return {};
708
+ }
709
+ }
710
+
711
+ scheduleHeadersUpdate () {
712
+ this.mcpConfig.headers = this.getHeadersFromForm();
713
+
714
+ if (this._headersUpdateTimer) {
715
+ clearTimeout(this._headersUpdateTimer);
716
+ }
717
+ this._headersUpdateTimer = setTimeout(() => {
718
+ this.applyHeadersUpdate().catch(err => console.warn('Apply headers failed:', err));
719
+ }, 600);
720
+ }
721
+
722
+ async applyHeadersUpdate () {
723
+ if (!this.currentServer || !this.currentServer.name) {
724
+ return;
725
+ }
726
+ const headers = this.getHeadersFromForm();
727
+ try {
728
+ const resp = await fetch(`${API_BASE}/api/mcp/headers`, {
729
+ method: 'POST',
730
+ headers: { 'Content-Type': 'application/json' },
731
+ body: JSON.stringify({ serverName: this.currentServer.name, headers }),
732
+ });
733
+ const data = await resp.json();
734
+ if (!resp.ok || !data.success) {
735
+ this.showToast('Failed to apply headers: ' + (data.error || resp.statusText), 'error');
736
+ return;
737
+ }
738
+ if (data.config) {
739
+ this.currentServer = { ...this.currentServer, ...data.config };
740
+ this.renderServerInfo();
741
+ }
742
+ this.showToast('Headers applied', 'success');
743
+ } catch (e) {
744
+ this.showToast('Failed to apply headers: ' + (e?.message || e), 'error');
745
+ }
746
+ }
747
+
748
+ getHeadersFromForm () {
749
+ const headers = {};
750
+
751
+ if (this.usedHeaders.length === 0) {
752
+ return headers;
753
+ }
754
+
755
+ this.usedHeaders.forEach(header => {
756
+ const input = document.getElementById(`header_${header.name}`);
757
+ if (input && input.value.trim()) {
758
+ headers[header.name] = input.value.trim();
759
+ }
760
+ });
761
+
762
+ return headers;
763
+ }
764
+
765
+ isOwnService () {
766
+ return this.defaultMcpUrl && this.serverUrlInput.value.trim() === this.defaultMcpUrl;
767
+ }
768
+
769
+ async autoFillAuthHeader () {
770
+ if (!this.authEnabled) {return;}
771
+
772
+ const hasAuthHeader = this.usedHeaders.some(h => h.name === 'Authorization');
773
+ if (!hasAuthHeader) {return;}
774
+
775
+ // Skip if localStorage already has a saved value for this URL's Authorization header
776
+ const savedHeaders = this.loadHeaderValuesFromStorage();
777
+ if (savedHeaders['Authorization']) {return;}
778
+
779
+ try {
780
+ const response = await fetch(`${API_BASE}/api/auth-token`);
781
+ if (!response.ok) {return;}
782
+
783
+ const data = await response.json();
784
+ this._currentAuthType = data.authType;
785
+
786
+ const input = document.getElementById('header_Authorization');
787
+ if (input) {
788
+ input.value = data.token;
789
+ this.updateHeaderBorder(input);
790
+ this.saveHeaderValuesToStorage();
791
+ this.scheduleHeadersUpdate();
792
+ }
793
+
794
+ // Start JWT refresh interval if connecting to own service
795
+ if (data.authType === 'jwtToken' && this.isOwnService()) {
796
+ this.startAuthRefresh();
797
+ }
798
+ } catch (e) {
799
+ console.warn('Failed to auto-fill auth header:', e);
800
+ }
801
+ }
802
+
803
+ startAuthRefresh () {
804
+ this.stopAuthRefresh();
805
+ this._authRefreshInterval = setInterval(async () => {
806
+ try {
807
+ const response = await fetch(`${API_BASE}/api/auth-token/refresh`, { method: 'POST' });
808
+ if (!response.ok) {return;}
809
+
810
+ const data = await response.json();
811
+ const input = document.getElementById('header_Authorization');
812
+ if (input) {
813
+ input.value = data.token;
814
+ this.saveHeaderValuesToStorage();
815
+ this.scheduleHeadersUpdate();
816
+ }
817
+ } catch (e) {
818
+ console.warn('Failed to refresh auth token:', e);
819
+ }
820
+ }, 4 * 60 * 1000); // every 4 minutes
821
+ }
822
+
823
+ stopAuthRefresh () {
824
+ if (this._authRefreshInterval) {
825
+ clearInterval(this._authRefreshInterval);
826
+ this._authRefreshInterval = null;
827
+ }
828
+ }
829
+
830
+ resetConnectionForm () {
831
+ this.stopAuthRefresh();
832
+ this.mcpConnectionForm.reset();
833
+ this.serverUrlInput.value = '';
834
+ this.transportSelect.value = 'http';
835
+ this.headersSection.style.display = 'none';
836
+ this.dynamicHeaders.innerHTML = '';
837
+ this.usedHeaders = [];
838
+ this.pendingConnectionData = null;
839
+ this.mcpConfig = {
840
+ url: null,
841
+ transport: 'http',
842
+ headers: {},
843
+ name: null,
844
+ };
845
+ window.history.replaceState({}, document.title, window.location.pathname);
846
+ localStorage.removeItem('mcpAgentFormValues');
847
+ }
848
+
849
+ async loadCurrentServer () {
850
+ try {
851
+ const response = await fetch(`${API_BASE}/api/mcp/servers`);
852
+ const servers = await response.json();
853
+
854
+ if (servers && servers.length > 0) {
855
+ this.currentServer = servers[0];
856
+ this.updateConnectionStatus();
857
+ this.renderServerInfo();
858
+ } else {
859
+ this.currentServer = null;
860
+ this.updateConnectionStatus();
861
+ this.renderServerInfo();
862
+ }
863
+
864
+ } catch (error) {
865
+ console.error('Error loading current server:', error);
866
+ this.currentServer = null;
867
+ this.updateConnectionStatus();
868
+ this.renderServerInfo();
869
+ }
870
+ }
871
+
872
+ renderServerInfo () {
873
+ if (!this.currentServer) {
874
+ this.connectedServersContainer.innerHTML = '';
875
+ return;
876
+ }
877
+
878
+ const server = this.currentServer;
879
+ const toolCount = server.tools ? server.tools.length : 0;
880
+
881
+ if (server.isConnected) {
882
+ this.connectedServersContainer.innerHTML = `
883
+ <div class="server-status-row">
884
+ <span class="server-status connected">${toolCount} tools <span class="material-icons-round">check_circle</span> connected</span>
885
+ <button type="button" class="btn btn-danger disconnect-btn"><span class="material-icons-round">link_off</span>Disconnect</button>
886
+ </div>`;
887
+ } else {
888
+ this.connectedServersContainer.innerHTML = `
889
+ <div class="server-status-row">
890
+ <span class="server-status disconnected"><span class="material-icons-round">cancel</span>Disconnected</span>
891
+ <button type="button" class="btn btn-secondary reconnect-btn"><span class="material-icons-round">refresh</span>Reconnect</button>
892
+ </div>`;
893
+ }
894
+
895
+ this.connectedServersContainer.querySelector('.disconnect-btn')?.addEventListener('click', () => {
896
+ this.disconnectServer();
897
+ });
898
+
899
+ this.connectedServersContainer.querySelector('.reconnect-btn')?.addEventListener('click', () => {
900
+ this.handleReconnect();
901
+ });
902
+ }
903
+
904
+ async disconnectServer () {
905
+ if (!this.currentServer) {
906
+ return;
907
+ }
908
+
909
+ this.stopAuthRefresh();
910
+
911
+ try {
912
+ const response = await fetch(`${API_BASE}/api/mcp/disconnect/${this.currentServer.name}`, { method: 'POST' });
913
+
914
+ if (response.ok) {
915
+ this.showToast(`Disconnected from ${this.currentServer.name}`, 'success');
916
+ this.currentServer = null;
917
+ this.mcpConfig = {
918
+ url: null,
919
+ transport: 'http',
920
+ headers: {},
921
+ name: null,
922
+ };
923
+ await this.loadCurrentServer();
924
+ this.updateConnectionStatus();
925
+ } else {
926
+ this.showToast('Failed to disconnect', 'error');
927
+ }
928
+
929
+ } catch (error) {
930
+ console.error('Disconnect error:', error);
931
+ this.showToast('Disconnect failed: ' + error.message, 'error');
932
+ }
933
+ }
934
+
935
+ async handleReconnect () {
936
+ if (!this.currentServer) {
937
+ return;
938
+ }
939
+
940
+ const connectionData = {
941
+ name: this.currentServer.name,
942
+ url: this.currentServer.url,
943
+ transport: this.currentServer.transport || 'http',
944
+ headers: this.currentServer.headers || {},
945
+ };
946
+
947
+ this.showLoading('Reconnecting to MCP server...');
948
+
949
+ try {
950
+ const response = await fetch(`${API_BASE}/api/mcp/connect`, {
951
+ method: 'POST',
952
+ headers: { 'Content-Type': 'application/json' },
953
+ body: JSON.stringify(connectionData),
954
+ });
955
+
956
+ if (response.ok) {
957
+ const result = await response.json();
958
+ this.currentServer = {
959
+ ...connectionData,
960
+ isConnected: true,
961
+ tools: result.tools || [],
962
+ };
963
+
964
+ this.showToast(`Reconnected to ${this.currentServer.name}`, 'success');
965
+ await this.loadCurrentServer();
966
+ this.updateConnectionStatus();
967
+ } else {
968
+ const errorText = await response.text();
969
+ this.showToast(`Reconnection failed: ${errorText}`, 'error');
970
+ }
971
+ } catch (error) {
972
+ console.error('Reconnect error:', error);
973
+ this.showToast('Reconnection failed: ' + error.message, 'error');
974
+ } finally {
975
+ this.hideLoading();
976
+ }
977
+ }
978
+
979
+ updateConnectionStatus () {
980
+ if (!this.connectionStatus) {return;}
981
+ if (this.currentServer && this.currentServer.isConnected) {
982
+ this.connectionStatus.textContent = `Connected to ${this.currentServer.name}`;
983
+ this.connectionStatus.classList.add('connected');
984
+ } else {
985
+ this.connectionStatus.textContent = 'Not Connected';
986
+ this.connectionStatus.classList.remove('connected');
987
+ }
988
+ }
989
+
990
+ handleInputChange () {
991
+ const { length } = this.messageInput.value;
992
+ this.charCount.textContent = `${length}/40000`;
993
+
994
+ const isEmpty = this.messageInput.value.trim() === '';
995
+ this.sendButton.disabled = isEmpty;
996
+
997
+ if (length >= 3800) {
998
+ this.charCount.style.color = '#e74c3c';
999
+ } else if (length >= 3500) {
1000
+ this.charCount.style.color = '#f39c12';
1001
+ } else {
1002
+ this.charCount.style.color = '#95a5a6';
1003
+ }
1004
+ }
1005
+
1006
+ handleKeyDown (event) {
1007
+ if (event.key === 'Enter' && !event.shiftKey) {
1008
+ event.preventDefault();
1009
+ this.sendMessage();
1010
+ }
1011
+ }
1012
+
1013
+ async sendMessage () {
1014
+ const message = this.messageInput.value.trim();
1015
+ if (!message) {return;}
1016
+
1017
+ if (!this.validateCustomModelSettings()) {
1018
+ return;
1019
+ }
1020
+
1021
+ this.addMessage(message, 'user');
1022
+
1023
+ this.messageInput.value = '';
1024
+ this.handleInputChange();
1025
+ this.messageInput.style.height = 'auto';
1026
+
1027
+ this.showTypingIndicator();
1028
+
1029
+ try {
1030
+ const modelConfig = this.getModelConfig();
1031
+
1032
+ const requestData = {
1033
+ message: message,
1034
+ sessionId: this.currentSessionId,
1035
+ agentPrompt: trim(this.systemPromptTextarea.value) || undefined,
1036
+ customPrompt: trim(this.customPromptTextarea.value) || undefined,
1037
+ model: modelConfig.model,
1038
+ useStreaming: false,
1039
+ mcpConfig: this.mcpConfig.url ? {
1040
+ url: this.mcpConfig.url,
1041
+ transport: this.mcpConfig.transport,
1042
+ headers: this.mcpConfig.headers,
1043
+ name: this.mcpConfig.name,
1044
+ } : undefined,
1045
+ modelConfig: modelConfig,
1046
+ };
1047
+
1048
+ const response = await fetch(`${API_BASE}/api/chat/message`, {
1049
+ method: 'POST',
1050
+ headers: { 'Content-Type': 'application/json' },
1051
+ body: JSON.stringify(requestData),
1052
+ });
1053
+
1054
+ if (!response.ok) {
1055
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1056
+ }
1057
+
1058
+ const result = await response.json();
1059
+
1060
+ this.currentSessionId = result.sessionId;
1061
+
1062
+ this.addMessage(result.message, 'assistant', result.metadata);
1063
+
1064
+ } catch (error) {
1065
+ console.error('Send message error:', error);
1066
+ this.addMessage(`Error: ${error.message}`, 'assistant', { error: true });
1067
+ this.showToast('Failed to send message: ' + error.message, 'error');
1068
+ } finally {
1069
+ this.hideTypingIndicator();
1070
+ }
1071
+ }
1072
+
1073
+ addMessage (text, sender, metadata = {}) {
1074
+ const messageId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1075
+ const messageDiv = document.createElement('div');
1076
+ messageDiv.className = `message ${sender}`;
1077
+ messageDiv.dataset.messageId = messageId;
1078
+
1079
+ if (metadata.error) {
1080
+ messageDiv.classList.add('error');
1081
+ }
1082
+
1083
+ const avatar = document.createElement('div');
1084
+ avatar.className = 'message-avatar';
1085
+ avatar.innerHTML = sender === 'user' ? '<span class="material-icons-round">person</span>' : '<span class="material-icons-round">smart_toy</span>';
1086
+
1087
+ const content = document.createElement('div');
1088
+ content.className = 'message-content';
1089
+
1090
+ if (sender === 'assistant' && !metadata.error) {
1091
+ this.messageFormats[messageId] = this.defaultDisplayFormat;
1092
+ this.messageTexts[messageId] = text;
1093
+
1094
+ const formatToggle = this.createFormatToggle(messageId);
1095
+ content.appendChild(formatToggle);
1096
+ }
1097
+
1098
+ const messageText = document.createElement('div');
1099
+ messageText.className = 'message-text';
1100
+ messageText.dataset.messageId = messageId;
1101
+
1102
+ if (sender === 'assistant' && !metadata.error) {
1103
+ const format = this.messageFormats[messageId];
1104
+ this.renderMessageContent(messageText, text, format);
1105
+ } else {
1106
+ messageText.textContent = text;
1107
+ }
1108
+
1109
+ const messageTime = document.createElement('div');
1110
+ messageTime.className = 'message-time';
1111
+ messageTime.textContent = new Date().toLocaleTimeString();
1112
+
1113
+ content.appendChild(messageText);
1114
+ content.appendChild(messageTime);
1115
+
1116
+ if (sender === 'assistant' && metadata && !metadata.error) {
1117
+ if (metadata.tools_used && metadata.tools_used.length > 0) {
1118
+ const toolsUsed = document.createElement('div');
1119
+ toolsUsed.className = 'message-tools';
1120
+ toolsUsed.innerHTML = `<small class="a-info">Tools used: ${metadata.tools_used.join(', ')}</small>`;
1121
+ content.appendChild(toolsUsed);
1122
+ }
1123
+
1124
+ if (metadata.response_time) {
1125
+ const responseTime = document.createElement('div');
1126
+ responseTime.className = 'message-timing';
1127
+ responseTime.innerHTML = `<small class="a-info">Response time: ${metadata.response_time}ms</small>`;
1128
+ content.appendChild(responseTime);
1129
+ }
1130
+ }
1131
+
1132
+ messageDiv.appendChild(avatar);
1133
+ messageDiv.appendChild(content);
1134
+
1135
+ if (sender === 'user') {
1136
+ messageDiv.addEventListener('dblclick', () => {
1137
+ const currentValue = this.messageInput.value;
1138
+ const newValue = currentValue ? currentValue + ' ' + text : text;
1139
+ this.messageInput.value = newValue;
1140
+ this.messageInput.focus();
1141
+ this.handleInputChange();
1142
+ });
1143
+ messageDiv.style.cursor = 'pointer';
1144
+ messageDiv.title = 'Double-click to add text to input field';
1145
+ }
1146
+
1147
+ this.chatMessages.appendChild(messageDiv);
1148
+ this.scrollToBottom();
1149
+ }
1150
+
1151
+ showTypingIndicator () {
1152
+ this.typingIndicator.classList.add('visible');
1153
+ }
1154
+
1155
+ hideTypingIndicator () {
1156
+ this.typingIndicator.classList.remove('visible');
1157
+ }
1158
+
1159
+ clearChat () {
1160
+ const welcomeMessage = this.chatMessages.querySelector('.message.welcome');
1161
+ this.chatMessages.innerHTML = '';
1162
+ if (welcomeMessage) {
1163
+ this.chatMessages.appendChild(welcomeMessage);
1164
+ }
1165
+
1166
+ this.currentSessionId = null;
1167
+
1168
+ this.showToast('Chat cleared', 'success');
1169
+ }
1170
+
1171
+ scrollToBottom () {
1172
+ setTimeout(() => {
1173
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
1174
+ }, 100);
1175
+ }
1176
+
1177
+ showLoading (message = 'Loading...') {
1178
+ this.loadingOverlay.querySelector('span').textContent = message;
1179
+ this.loadingOverlay.style.display = 'flex';
1180
+ }
1181
+
1182
+ hideLoading () {
1183
+ this.loadingOverlay.style.display = 'none';
1184
+ }
1185
+
1186
+ showToast (message, type = 'info') {
1187
+ const toast = document.createElement('div');
1188
+ toast.className = `toast ${type}`;
1189
+
1190
+ const icon = {
1191
+ 'success': 'check_circle',
1192
+ 'error': 'error',
1193
+ 'warning': 'warning',
1194
+ 'info': 'info',
1195
+ }[type] || 'info';
1196
+
1197
+ toast.innerHTML = `
1198
+ <span class="material-icons-round">${icon}</span>
1199
+ <span>${message}</span>
1200
+ `;
1201
+
1202
+ this.toastContainer.appendChild(toast);
1203
+
1204
+ setTimeout(() => {
1205
+ if (toast.parentNode) {
1206
+ toast.parentNode.removeChild(toast);
1207
+ }
1208
+ }, 5000);
1209
+
1210
+ toast.addEventListener('click', () => {
1211
+ if (toast.parentNode) {
1212
+ toast.parentNode.removeChild(toast);
1213
+ }
1214
+ });
1215
+ }
1216
+
1217
+ handleModelSelectChange () {
1218
+ const isOther = this.modelSelect.value === 'other';
1219
+ this.customModelSettings.style.display = isOther ? 'block' : 'none';
1220
+ }
1221
+
1222
+ validateCustomModelSettings () {
1223
+ if (this.modelSelect.value !== 'other') {
1224
+ return true;
1225
+ }
1226
+
1227
+ const baseURL = trim(this.customBaseUrl.value);
1228
+ const apiKey = trim(this.customApiKey.value);
1229
+ const modelName = trim(this.customModelName.value);
1230
+ const temperature = this.modelTemperature.value;
1231
+ const maxTokens = this.modelMaxTokens.value;
1232
+
1233
+ const missingFields = [];
1234
+ if (!baseURL) {missingFields.push('Base URL');}
1235
+ if (!apiKey) {missingFields.push('API Key');}
1236
+ if (!modelName) {missingFields.push('Model Name');}
1237
+ if (!temperature) {missingFields.push('Temperature');}
1238
+ if (!maxTokens) {missingFields.push('Max Tokens');}
1239
+
1240
+ if (missingFields.length > 0) {
1241
+ this.showToast(`Missing required fields: ${missingFields.join(', ')}`, 'error');
1242
+ return false;
1243
+ }
1244
+
1245
+ return true;
1246
+ }
1247
+
1248
+ getModelConfig () {
1249
+ const isOther = this.modelSelect.value === 'other';
1250
+ const t = parseFloat(this.modelTemperature.value);
1251
+ const temperature = Number.isNaN(t) ? 0.1 : t;
1252
+ const maxTokens = parseInt(this.modelMaxTokens.value, 10) || 2048;
1253
+ const maxTurns = parseInt(this.modelMaxTurns.value, 10) || 10;
1254
+ const toolResultLimitChars = parseInt(this.toolResultLimitChars.value, 10) || 20000;
1255
+
1256
+ if (isOther) {
1257
+ return {
1258
+ baseURL: trim(this.customBaseUrl.value),
1259
+ apiKey: trim(this.customApiKey.value),
1260
+ model: trim(this.customModelName.value),
1261
+ temperature: temperature,
1262
+ maxTokens: maxTokens,
1263
+ maxTurns: maxTurns,
1264
+ toolResultLimitChars: toolResultLimitChars,
1265
+ };
1266
+ }
1267
+
1268
+ return {
1269
+ model: this.modelSelect.value,
1270
+ temperature: temperature,
1271
+ maxTokens: maxTokens,
1272
+ maxTurns: maxTurns,
1273
+ toolResultLimitChars: toolResultLimitChars,
1274
+ };
1275
+ }
1276
+
1277
+ handleServerUrlChange () {
1278
+ this.stopAuthRefresh();
1279
+ let url = this.serverUrlInput.value.trim();
1280
+
1281
+ if (url) {
1282
+ url = url.replace(/\/+$/, '');
1283
+
1284
+ try {
1285
+ const parsedUrl = new URL(url);
1286
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
1287
+ url = url + '/mcp';
1288
+ }
1289
+ } catch {
1290
+ if (!url.includes('/')) {
1291
+ url = url + '/mcp';
1292
+ } else if (url.endsWith('/')) {
1293
+ url = url + 'mcp';
1294
+ } else if (!url.split('/').slice(1).join('/')) {
1295
+ url = url + '/mcp';
1296
+ }
1297
+ }
1298
+
1299
+ this.serverUrlInput.value = url;
1300
+ }
1301
+
1302
+ this.saveFormValuesToStorage();
1303
+ }
1304
+
1305
+ saveFormValuesToStorage () {
1306
+ const formData = {
1307
+ serverUrl: this.serverUrlInput.value,
1308
+ transport: this.transportSelect.value,
1309
+ model: this.modelSelect.value,
1310
+ agentPrompt: trim(this.systemPromptTextarea.value),
1311
+ customPrompt: trim(this.customPromptTextarea.value),
1312
+ customBaseUrl: trim(this.customBaseUrl.value),
1313
+ customApiKey: trim(this.customApiKey.value),
1314
+ customModelName: trim(this.customModelName.value),
1315
+ modelTemperature: this.modelTemperature.value,
1316
+ modelMaxTokens: this.modelMaxTokens.value,
1317
+ modelMaxTurns: this.modelMaxTurns.value,
1318
+ toolResultLimitChars: this.toolResultLimitChars.value,
1319
+ };
1320
+ localStorage.setItem('mcpAgentFormValues', JSON.stringify(formData));
1321
+ }
1322
+
1323
+ loadFormValuesFromURL () {
1324
+ try {
1325
+ const params = new URLSearchParams(window.location.search);
1326
+ const serverUrl = params.get('serverUrl');
1327
+ const transport = params.get('transport');
1328
+
1329
+ if (serverUrl) {
1330
+ this.serverUrlInput.value = serverUrl;
1331
+ }
1332
+ if (transport) {
1333
+ this.transportSelect.value = transport;
1334
+ }
1335
+ } catch (error) {
1336
+ console.error('Error loading form values from URL:', error);
1337
+ }
1338
+ }
1339
+
1340
+ loadFormValuesFromStorage () {
1341
+ try {
1342
+ const stored = localStorage.getItem('mcpAgentFormValues');
1343
+ if (stored) {
1344
+ const formData = JSON.parse(stored);
1345
+ if (formData.serverUrl) {this.serverUrlInput.value = formData.serverUrl;}
1346
+ if (formData.transport) {this.transportSelect.value = formData.transport;}
1347
+ if (formData.model) {this.modelSelect.value = formData.model;}
1348
+ if (formData.agentPrompt) {this.systemPromptTextarea.value = trim(formData.agentPrompt);}
1349
+ if (formData.customPrompt) {this.customPromptTextarea.value = trim(formData.customPrompt);}
1350
+ if (formData.customBaseUrl) {this.customBaseUrl.value = formData.customBaseUrl;}
1351
+ if (formData.customApiKey) {this.customApiKey.value = formData.customApiKey;}
1352
+ if (formData.customModelName) {this.customModelName.value = formData.customModelName;}
1353
+ if (formData.modelTemperature) {this.modelTemperature.value = formData.modelTemperature;}
1354
+ if (formData.modelMaxTokens) {this.modelMaxTokens.value = formData.modelMaxTokens;}
1355
+ if (formData.modelMaxTurns) {this.modelMaxTurns.value = formData.modelMaxTurns;}
1356
+ if (formData.toolResultLimitChars) {this.toolResultLimitChars.value = formData.toolResultLimitChars;}
1357
+ this.handleModelSelectChange();
1358
+ }
1359
+ } catch (error) {
1360
+ console.error('Error loading form values from storage:', error);
1361
+ }
1362
+ }
1363
+
1364
+ getSavedUrls () {
1365
+ try {
1366
+ const saved = localStorage.getItem('mcpSavedUrls');
1367
+ return saved ? JSON.parse(saved) : [];
1368
+ } catch (error) {
1369
+ console.error('Error loading saved URLs:', error);
1370
+ return [];
1371
+ }
1372
+ }
1373
+
1374
+ saveSavedUrls (urls) {
1375
+ try {
1376
+ localStorage.setItem('mcpSavedUrls', JSON.stringify(urls));
1377
+ } catch (error) {
1378
+ console.error('Error saving URLs:', error);
1379
+ }
1380
+ }
1381
+
1382
+ addUrlToSaved (url) {
1383
+ if (!url || url.trim() === '') {
1384
+ return;
1385
+ }
1386
+
1387
+ url = url.trim();
1388
+ let savedUrls = this.getSavedUrls();
1389
+
1390
+ savedUrls = savedUrls.filter(savedUrl => savedUrl !== url);
1391
+
1392
+ savedUrls.unshift(url);
1393
+
1394
+ savedUrls = savedUrls.slice(0, 10);
1395
+
1396
+ this.saveSavedUrls(savedUrls);
1397
+ this.renderSavedUrls();
1398
+ }
1399
+
1400
+ removeUrlFromSaved (url) {
1401
+ let savedUrls = this.getSavedUrls();
1402
+ savedUrls = savedUrls.filter(savedUrl => savedUrl !== url);
1403
+ this.saveSavedUrls(savedUrls);
1404
+ this.renderSavedUrls();
1405
+ }
1406
+
1407
+ renderSavedUrls () {
1408
+ const savedUrls = this.getSavedUrls();
1409
+ this.savedUrlsList.innerHTML = '';
1410
+
1411
+ if (savedUrls.length === 0) {
1412
+ const emptyItem = document.createElement('div');
1413
+ emptyItem.className = 'dropdown-item disabled';
1414
+ emptyItem.innerHTML = '<span style="color: rgba(255,255,255,0.5);">No saved URLs</span>';
1415
+ this.savedUrlsList.appendChild(emptyItem);
1416
+ return;
1417
+ }
1418
+
1419
+ savedUrls.forEach(url => {
1420
+ const item = document.createElement('div');
1421
+ item.className = 'dropdown-item';
1422
+
1423
+ item.innerHTML = `
1424
+ <div class="url-item">
1425
+ <span class="url-text" title="${url}">${url}</span>
1426
+ <button class="delete-btn" title="Delete URL">
1427
+ <span class="material-icons-round" style="font-size: 16px;">close</span>
1428
+ </button>
1429
+ </div>
1430
+ `;
1431
+
1432
+ item.querySelector('.url-text').addEventListener('click', () => {
1433
+ this.selectUrl(url);
1434
+ });
1435
+
1436
+ item.querySelector('.delete-btn').addEventListener('click', (e) => {
1437
+ e.stopPropagation();
1438
+ this.removeUrlFromSaved(url);
1439
+ });
1440
+
1441
+ this.savedUrlsList.appendChild(item);
1442
+ });
1443
+ }
1444
+
1445
+ selectUrl (url) {
1446
+ this.serverUrlInput.value = url;
1447
+ this.handleServerUrlChange();
1448
+ this.closeUrlDropdown();
1449
+ this.autoConnect();
1450
+ }
1451
+
1452
+ toggleUrlDropdown (e) {
1453
+ e.preventDefault();
1454
+ e.stopPropagation();
1455
+
1456
+ const isVisible = this.serverUrlDropdownList.style.display !== 'none';
1457
+
1458
+ if (isVisible) {
1459
+ this.closeUrlDropdown();
1460
+ } else {
1461
+ this.openUrlDropdown();
1462
+ }
1463
+ }
1464
+
1465
+ openUrlDropdown () {
1466
+ this.renderSavedUrls();
1467
+ this.serverUrlDropdownList.style.display = 'block';
1468
+ this.serverUrlDropdown.classList.add('active');
1469
+
1470
+ const addNewItem = this.serverUrlDropdownList.querySelector('.add-new');
1471
+ if (addNewItem) {
1472
+ addNewItem.addEventListener('click', () => {
1473
+ this.addCurrentUrlToSaved();
1474
+ });
1475
+ }
1476
+ }
1477
+
1478
+ closeUrlDropdown () {
1479
+ this.serverUrlDropdownList.style.display = 'none';
1480
+ this.serverUrlDropdown.classList.remove('active');
1481
+ }
1482
+
1483
+ addCurrentUrlToSaved () {
1484
+ const currentUrl = this.serverUrlInput.value.trim();
1485
+ if (currentUrl) {
1486
+ this.addUrlToSaved(currentUrl);
1487
+ this.closeUrlDropdown();
1488
+ this.showToast('URL added to saved', 'success');
1489
+ }
1490
+ }
1491
+
1492
+ handleClickOutside (e) {
1493
+ const container = e.target.closest('.custom-select-container');
1494
+ if (!container) {
1495
+ this.closeUrlDropdown();
1496
+ }
1497
+ }
1498
+ }
1499
+
1500
+ // Initialize the app when DOM is loaded
1501
+ document.addEventListener('DOMContentLoaded', () => {
1502
+ window.mcpAgentTester = new McpAgentTester();
1503
+ });