@desplega.ai/qa-use 2.0.1

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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1003 -0
  3. package/bin/qa-use.js +7 -0
  4. package/dist/lib/api/index.d.ts +296 -0
  5. package/dist/lib/api/index.d.ts.map +1 -0
  6. package/dist/lib/api/index.js +564 -0
  7. package/dist/lib/api/index.js.map +1 -0
  8. package/dist/lib/api/sse.d.ts +33 -0
  9. package/dist/lib/api/sse.d.ts.map +1 -0
  10. package/dist/lib/api/sse.js +97 -0
  11. package/dist/lib/api/sse.js.map +1 -0
  12. package/dist/lib/browser/index.d.ts +28 -0
  13. package/dist/lib/browser/index.d.ts.map +1 -0
  14. package/dist/lib/browser/index.js +145 -0
  15. package/dist/lib/browser/index.js.map +1 -0
  16. package/dist/lib/env/index.d.ts +41 -0
  17. package/dist/lib/env/index.d.ts.map +1 -0
  18. package/dist/lib/env/index.js +125 -0
  19. package/dist/lib/env/index.js.map +1 -0
  20. package/dist/lib/tunnel/index.d.ts +38 -0
  21. package/dist/lib/tunnel/index.d.ts.map +1 -0
  22. package/dist/lib/tunnel/index.js +154 -0
  23. package/dist/lib/tunnel/index.js.map +1 -0
  24. package/dist/package.json +100 -0
  25. package/dist/src/cli/commands/info.d.ts +6 -0
  26. package/dist/src/cli/commands/info.d.ts.map +1 -0
  27. package/dist/src/cli/commands/info.js +32 -0
  28. package/dist/src/cli/commands/info.js.map +1 -0
  29. package/dist/src/cli/commands/mcp.d.ts +6 -0
  30. package/dist/src/cli/commands/mcp.d.ts.map +1 -0
  31. package/dist/src/cli/commands/mcp.js +45 -0
  32. package/dist/src/cli/commands/mcp.js.map +1 -0
  33. package/dist/src/cli/commands/setup.d.ts +6 -0
  34. package/dist/src/cli/commands/setup.d.ts.map +1 -0
  35. package/dist/src/cli/commands/setup.js +59 -0
  36. package/dist/src/cli/commands/setup.js.map +1 -0
  37. package/dist/src/cli/commands/test/index.d.ts +6 -0
  38. package/dist/src/cli/commands/test/index.d.ts.map +1 -0
  39. package/dist/src/cli/commands/test/index.js +15 -0
  40. package/dist/src/cli/commands/test/index.js.map +1 -0
  41. package/dist/src/cli/commands/test/init.d.ts +6 -0
  42. package/dist/src/cli/commands/test/init.d.ts.map +1 -0
  43. package/dist/src/cli/commands/test/init.js +64 -0
  44. package/dist/src/cli/commands/test/init.js.map +1 -0
  45. package/dist/src/cli/commands/test/list.d.ts +6 -0
  46. package/dist/src/cli/commands/test/list.d.ts.map +1 -0
  47. package/dist/src/cli/commands/test/list.js +70 -0
  48. package/dist/src/cli/commands/test/list.js.map +1 -0
  49. package/dist/src/cli/commands/test/run.d.ts +6 -0
  50. package/dist/src/cli/commands/test/run.d.ts.map +1 -0
  51. package/dist/src/cli/commands/test/run.js +95 -0
  52. package/dist/src/cli/commands/test/run.js.map +1 -0
  53. package/dist/src/cli/commands/test/validate.d.ts +6 -0
  54. package/dist/src/cli/commands/test/validate.d.ts.map +1 -0
  55. package/dist/src/cli/commands/test/validate.js +70 -0
  56. package/dist/src/cli/commands/test/validate.js.map +1 -0
  57. package/dist/src/cli/index.d.ts +6 -0
  58. package/dist/src/cli/index.d.ts.map +1 -0
  59. package/dist/src/cli/index.js +21 -0
  60. package/dist/src/cli/index.js.map +1 -0
  61. package/dist/src/cli/lib/config.d.ts +36 -0
  62. package/dist/src/cli/lib/config.d.ts.map +1 -0
  63. package/dist/src/cli/lib/config.js +89 -0
  64. package/dist/src/cli/lib/config.js.map +1 -0
  65. package/dist/src/cli/lib/loader.d.ts +49 -0
  66. package/dist/src/cli/lib/loader.d.ts.map +1 -0
  67. package/dist/src/cli/lib/loader.js +122 -0
  68. package/dist/src/cli/lib/loader.js.map +1 -0
  69. package/dist/src/cli/lib/output.d.ts +53 -0
  70. package/dist/src/cli/lib/output.d.ts.map +1 -0
  71. package/dist/src/cli/lib/output.js +133 -0
  72. package/dist/src/cli/lib/output.js.map +1 -0
  73. package/dist/src/cli/lib/runner.d.ts +23 -0
  74. package/dist/src/cli/lib/runner.d.ts.map +1 -0
  75. package/dist/src/cli/lib/runner.js +40 -0
  76. package/dist/src/cli/lib/runner.js.map +1 -0
  77. package/dist/src/http-server.d.ts +14 -0
  78. package/dist/src/http-server.d.ts.map +1 -0
  79. package/dist/src/http-server.js +145 -0
  80. package/dist/src/http-server.js.map +1 -0
  81. package/dist/src/index.d.ts +9 -0
  82. package/dist/src/index.d.ts.map +1 -0
  83. package/dist/src/index.js +21 -0
  84. package/dist/src/index.js.map +1 -0
  85. package/dist/src/server.d.ts +58 -0
  86. package/dist/src/server.d.ts.map +1 -0
  87. package/dist/src/server.js +2376 -0
  88. package/dist/src/server.js.map +1 -0
  89. package/dist/src/tunnel-mode.d.ts +13 -0
  90. package/dist/src/tunnel-mode.d.ts.map +1 -0
  91. package/dist/src/tunnel-mode.js +159 -0
  92. package/dist/src/tunnel-mode.js.map +1 -0
  93. package/dist/src/types/test-definition.d.ts +320 -0
  94. package/dist/src/types/test-definition.d.ts.map +1 -0
  95. package/dist/src/types/test-definition.js +11 -0
  96. package/dist/src/types/test-definition.js.map +1 -0
  97. package/dist/src/types.d.ts +209 -0
  98. package/dist/src/types.d.ts.map +1 -0
  99. package/dist/src/types.js +34 -0
  100. package/dist/src/types.js.map +1 -0
  101. package/dist/src/utils/package.d.ts +12 -0
  102. package/dist/src/utils/package.d.ts.map +1 -0
  103. package/dist/src/utils/package.js +36 -0
  104. package/dist/src/utils/package.js.map +1 -0
  105. package/dist/src/utils/summary.d.ts +45 -0
  106. package/dist/src/utils/summary.d.ts.map +1 -0
  107. package/dist/src/utils/summary.js +198 -0
  108. package/dist/src/utils/summary.js.map +1 -0
  109. package/lib/api/index.ts +977 -0
  110. package/lib/api/sse.ts +112 -0
  111. package/lib/browser/index.ts +181 -0
  112. package/lib/env/index.ts +156 -0
  113. package/lib/tunnel/index.test.ts +344 -0
  114. package/lib/tunnel/index.ts +197 -0
  115. package/lib/tunnel/integration.test.ts +98 -0
  116. package/package.json +100 -0
  117. package/server.json +16 -0
@@ -0,0 +1,2376 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { BrowserManager } from '../lib/browser/index.js';
5
+ import { TunnelManager } from '../lib/tunnel/index.js';
6
+ import { ApiClient } from '../lib/api/index.js';
7
+ import { getName, getVersion } from './utils/package.js';
8
+ import { isTestCreatorDoneIntent } from './types.js';
9
+ import { generateEnhancedTestSummary, formatEnhancedTestReport, generateIssueStatistics, categorizeIssues, } from './utils/summary.js';
10
+ class BrowserSession {
11
+ session_id;
12
+ created_at;
13
+ browser;
14
+ tunnel;
15
+ ttl;
16
+ deadline;
17
+ session_type;
18
+ apiKey;
19
+ sessionIndex;
20
+ constructor(sessionId, browser, tunnel, sessionType, ttl = 30 * 60 * 1000, // 30 minutes default
21
+ apiKey, sessionIndex) {
22
+ this.session_id = sessionId;
23
+ this.created_at = Date.now();
24
+ this.browser = browser;
25
+ this.tunnel = tunnel;
26
+ this.ttl = ttl;
27
+ this.deadline = this.created_at + ttl;
28
+ this.session_type = sessionType;
29
+ this.apiKey = apiKey;
30
+ this.sessionIndex = sessionIndex;
31
+ }
32
+ refreshDeadline() {
33
+ this.deadline = Date.now() + this.ttl;
34
+ }
35
+ isExpired() {
36
+ return Date.now() > this.deadline;
37
+ }
38
+ async cleanup() {
39
+ try {
40
+ await this.tunnel.stopTunnel();
41
+ }
42
+ catch (error) {
43
+ // Ignore cleanup errors
44
+ }
45
+ try {
46
+ await this.browser.stopBrowser();
47
+ }
48
+ catch (error) {
49
+ // Ignore cleanup errors
50
+ }
51
+ }
52
+ }
53
+ class QAUseMcpServer {
54
+ server;
55
+ globalApiClient;
56
+ browserSessions = [];
57
+ DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
58
+ // Global browser session (single browser shared across all sessions)
59
+ globalBrowser = null;
60
+ globalTunnel = null;
61
+ globalSessionIndex = 0; // Always use index 0 for single browser
62
+ cleanupInterval = null;
63
+ constructor() {
64
+ this.server = new Server({
65
+ name: getName(),
66
+ version: getVersion(),
67
+ }, {
68
+ capabilities: {
69
+ tools: {},
70
+ resources: {},
71
+ prompts: {},
72
+ logging: {},
73
+ },
74
+ });
75
+ this.globalApiClient = new ApiClient();
76
+ this.setupTools();
77
+ this.setupResources();
78
+ this.setupPrompts();
79
+ this.startCleanupTask();
80
+ }
81
+ createSessionSummary(session) {
82
+ let task = '<Not specified>';
83
+ if (session.data.history?.length > 0) {
84
+ task = session.data.history[0].task;
85
+ }
86
+ return {
87
+ id: session.id,
88
+ status: session.status,
89
+ createdAt: session.created_at,
90
+ data: {
91
+ status: session.data?.status,
92
+ url: session.data?.liveview_url,
93
+ task,
94
+ test_id: session.data?.test_id,
95
+ agent_id: session.data?.agent_id,
96
+ liveview_url: session.data?.liveview_url,
97
+ hasPendingInput: !!session.data?.pending_user_input,
98
+ lastActivity: session.data?.last_done ? 'Recent activity available' : 'No recent activity',
99
+ historyCount: session.data?.history?.length ?? 0,
100
+ blocksCount: session.data?.blocks?.length ?? 0,
101
+ },
102
+ source: session.source,
103
+ note: 'Use monitor_qa_session for full details including history and blocks',
104
+ };
105
+ }
106
+ createTestSummary(test) {
107
+ return {
108
+ id: test.id,
109
+ name: test.name,
110
+ description: test.description && test.description.length > 60
111
+ ? test.description.substring(0, 60) + '...'
112
+ : test.description,
113
+ url: test.url,
114
+ task: test.task && test.task.length > 60 ? test.task.substring(0, 60) + '...' : test.task,
115
+ status: test.status,
116
+ created_at: test.created_at,
117
+ dependency_test_ids: test.dependency_test_ids,
118
+ note: 'Use find_automated_test({testId: "specific-id"}) for full details',
119
+ };
120
+ }
121
+ formatSessionProgress(session, elapsed, iterations) {
122
+ const status = session.data?.status || session.status;
123
+ const sessionId = session.id;
124
+ const liveviewUrl = session.data?.liveview_url;
125
+ const lastDone = session.data?.last_done;
126
+ const task = session.data?.task;
127
+ const history = session.data?.history || [];
128
+ const blocks = session.data?.blocks || [];
129
+ // Build rich progress context from last_done and recent history
130
+ let outcomeMessage = '';
131
+ let progressSummary = '';
132
+ let recentSteps = '';
133
+ // Extract outcome from last_done
134
+ if (lastDone) {
135
+ if (lastDone.message) {
136
+ outcomeMessage = lastDone.message;
137
+ }
138
+ if (lastDone.status) {
139
+ const statusEmoji = lastDone.status === 'failure' ? '❌' : lastDone.status === 'success' ? '✅' : '📋';
140
+ outcomeMessage = `${statusEmoji} ${lastDone.status.toUpperCase()}: ${outcomeMessage || 'Session completed'}`;
141
+ }
142
+ if (lastDone.reasoning) {
143
+ progressSummary =
144
+ lastDone.reasoning.length > 150
145
+ ? lastDone.reasoning.substring(0, 150) + '...'
146
+ : lastDone.reasoning;
147
+ }
148
+ }
149
+ // Get recent steps from history
150
+ if (history.length > 0) {
151
+ const latestTask = history[history.length - 1];
152
+ if (latestTask.intents && latestTask.intents.length > 0) {
153
+ const recentIntents = latestTask.intents.slice(-3); // Last 3 actions
154
+ const stepNames = recentIntents.map((intent) => intent.intent?.short_name || intent.intent?.type || 'Action');
155
+ recentSteps = stepNames.join(' → ');
156
+ }
157
+ }
158
+ // Execution stats
159
+ const totalBlocks = blocks.length;
160
+ const completedTasks = history.filter((h) => h.status === 'completed').length;
161
+ const failedTasks = history.filter((h) => h.status === 'failed').length;
162
+ // Check for enhanced test results
163
+ let enhancedTestInfo = '';
164
+ if (lastDone && isTestCreatorDoneIntent(lastDone)) {
165
+ const testType = lastDone.is_positive ? 'Positive' : 'Negative';
166
+ const resultIcon = lastDone.status === 'success' ? '✅' : '❌';
167
+ enhancedTestInfo = `\n${resultIcon} **${testType} Test**: ${lastDone.status.toUpperCase()}`;
168
+ if (lastDone.issues && lastDone.issues.length > 0) {
169
+ const categorized = categorizeIssues(lastDone.issues);
170
+ const criticalCount = categorized.critical.length;
171
+ const highCount = categorized.high.length;
172
+ enhancedTestInfo += `\n🔍 **Issues Found**: ${lastDone.issues.length} total`;
173
+ if (criticalCount > 0)
174
+ enhancedTestInfo += ` (${criticalCount} critical)`;
175
+ if (highCount > 0)
176
+ enhancedTestInfo += ` (${highCount} high)`;
177
+ }
178
+ if (lastDone.explanation) {
179
+ const shortExplanation = lastDone.explanation.length > 100
180
+ ? lastDone.explanation.substring(0, 100) + '...'
181
+ : lastDone.explanation;
182
+ enhancedTestInfo += `\n📝 **Explanation**: ${shortExplanation}`;
183
+ }
184
+ }
185
+ // Format based on whether this is a timeout scenario or instant check
186
+ if (elapsed !== undefined && iterations !== undefined) {
187
+ // This is a timeout scenario - session still running
188
+ return `⏳ **Session Still Running** (${elapsed}s elapsed)
189
+
190
+ 🎯 **Task**: ${task || 'QA Testing Session'}
191
+
192
+ 📍 **Current Status**: ${status}
193
+
194
+ ${outcomeMessage ? `🎯 **Latest Outcome**: ${outcomeMessage}` : ''}
195
+
196
+ ${recentSteps ? `🔄 **Recent Steps**: ${recentSteps}` : ''}
197
+
198
+ ${progressSummary ? `📋 **Progress Details**: ${progressSummary}` : ''}
199
+ ${enhancedTestInfo}
200
+
201
+ 📊 **Execution Stats**: ${totalBlocks} blocks generated, ${completedTasks} tasks completed${failedTasks > 0 ? `, ${failedTasks} tasks failed` : ''}
202
+
203
+ ${liveviewUrl ? `👀 **Watch Live**: ${liveviewUrl}` : ''}
204
+
205
+ ⏰ **Monitoring Info**: Checked ${iterations} times over ${elapsed}s
206
+
207
+ 🎯 **Next step**: monitor_qa_session({sessionId: "${sessionId}", wait_for_completion: true}) to continue monitoring`;
208
+ }
209
+ else {
210
+ // This is an instant status check
211
+ return `📊 **Session Status**: ${status}
212
+
213
+ 🎯 **Task**: ${task || 'QA Testing Session'}
214
+
215
+ ${outcomeMessage ? `🎯 **Latest Outcome**: ${outcomeMessage}` : ''}
216
+
217
+ ${recentSteps ? `🔄 **Recent Steps**: ${recentSteps}` : ''}
218
+
219
+ ${progressSummary ? `📋 **Progress Details**: ${progressSummary}` : ''}
220
+ ${enhancedTestInfo}
221
+
222
+ 📊 **Execution Stats**: ${totalBlocks} blocks generated, ${completedTasks} tasks completed${failedTasks > 0 ? `, ${failedTasks} tasks failed` : ''}
223
+
224
+ ${liveviewUrl ? `👀 **Watch Live**: ${liveviewUrl}` : ''}
225
+
226
+ ${status === 'active' ? '🎯 **Next step**: monitor_qa_session({sessionId: "' + sessionId + '", wait_for_completion: true}) to wait for completion' : ''}
227
+ ${status === 'pending' ? '❓ **Needs Input**: Check for pending_user_input and use interact_with_qa_session to respond' : ''}
228
+ ${status === 'closed' ? '✅ **Completed**: Session finished successfully' : ''}
229
+ ${status === 'idle' ? '⏸️ **Paused**: Session is idle, may need intervention' : ''}`;
230
+ }
231
+ }
232
+ formatSessionCompletion(session, elapsed, iterations) {
233
+ const status = session.data?.status || session.status;
234
+ const sessionId = session.id;
235
+ const liveviewUrl = session.data?.liveview_url;
236
+ const lastDone = session.data?.last_done;
237
+ const task = session.data?.task;
238
+ // Build final result context
239
+ let resultContext = '';
240
+ if (lastDone) {
241
+ if (typeof lastDone === 'string') {
242
+ resultContext = lastDone.length > 150 ? lastDone.substring(0, 150) + '...' : lastDone;
243
+ }
244
+ else if (lastDone.action || lastDone.description) {
245
+ resultContext = lastDone.action || lastDone.description || 'Session completed';
246
+ if (resultContext.length > 150) {
247
+ resultContext = resultContext.substring(0, 150) + '...';
248
+ }
249
+ }
250
+ }
251
+ const statusEmoji = status === 'closed' ? '✅' : '⏸️';
252
+ const statusText = status === 'closed' ? 'Completed Successfully' : 'Paused';
253
+ let baseReport = `${statusEmoji} **Session ${statusText}** (${elapsed}s total)
254
+
255
+ 🎯 **Task**: ${task || 'QA Testing Session'}
256
+
257
+ 📍 **Final Status**: ${status}
258
+
259
+ ${resultContext ? `📋 **Final Result**: ${resultContext}` : '✅ **Status**: Session completed'}
260
+
261
+ ${liveviewUrl ? `👀 **Recording**: ${liveviewUrl}` : ''}
262
+
263
+ ⏰ **Session Info**: Monitored for ${elapsed}s with ${iterations} status checks
264
+
265
+ 🎯 **Next step**: Session complete! You can now start a new session or view results${liveviewUrl ? ' in the recording' : ''}`;
266
+ // Add enhanced summary if TestCreatorDoneIntent is available
267
+ if (lastDone && isTestCreatorDoneIntent(lastDone)) {
268
+ try {
269
+ const enhancedSummary = generateEnhancedTestSummary(session.data);
270
+ const enhancedReport = formatEnhancedTestReport(enhancedSummary);
271
+ baseReport += '\n\n---\n\n' + enhancedReport;
272
+ // Add issue statistics if issues were found
273
+ if (enhancedSummary.discoveredIssues.length > 0) {
274
+ const stats = generateIssueStatistics(enhancedSummary.discoveredIssues);
275
+ baseReport += `\n\n## Issue Statistics\n`;
276
+ baseReport += `- Total Issues: ${stats.totalIssues}\n`;
277
+ baseReport += `- Critical/Blocker: ${stats.criticalCount}\n`;
278
+ baseReport += `- Most Common Type: ${stats.mostCommonType || 'N/A'}\n`;
279
+ }
280
+ }
281
+ catch (error) {
282
+ // Silently fail if enhanced summary generation fails
283
+ console.error('Failed to generate enhanced summary:', error);
284
+ }
285
+ }
286
+ return baseReport;
287
+ }
288
+ startCleanupTask() {
289
+ this.cleanupInterval = setInterval(() => {
290
+ this.cleanupExpiredSessions();
291
+ }, 10000); // Changed from 1000ms (1s) to 10000ms (10s) for better performance
292
+ }
293
+ async cleanupExpiredSessions() {
294
+ const expiredSessions = this.browserSessions.filter((session) => session.isExpired());
295
+ for (const session of expiredSessions) {
296
+ try {
297
+ // For dev/automated sessions, try to close gracefully via API
298
+ if (session.session_type === 'dev' || session.session_type === 'automated') {
299
+ try {
300
+ await this.globalApiClient.sendMessage({
301
+ sessionId: session.session_id,
302
+ action: 'close',
303
+ data: 'Session expired due to inactivity',
304
+ });
305
+ }
306
+ catch (error) {
307
+ // If API call fails, continue with cleanup
308
+ }
309
+ }
310
+ // NOTE: Do NOT cleanup browser/tunnel here since they are shared globally
311
+ // Only remove the session from tracking
312
+ this.browserSessions = this.browserSessions.filter((s) => s.session_id !== session.session_id);
313
+ }
314
+ catch (error) {
315
+ // Silent cleanup - continue with next session
316
+ }
317
+ }
318
+ }
319
+ getBrowserSession(sessionId) {
320
+ return this.browserSessions.find((s) => s.session_id === sessionId) || null;
321
+ }
322
+ /**
323
+ * Cleanup global browser and tunnel resources
324
+ */
325
+ async cleanupGlobalResources() {
326
+ if (this.globalTunnel) {
327
+ try {
328
+ await this.globalTunnel.stopTunnel();
329
+ }
330
+ catch {
331
+ // Silent cleanup
332
+ }
333
+ this.globalTunnel = null;
334
+ }
335
+ if (this.globalBrowser) {
336
+ try {
337
+ await this.globalBrowser.stopBrowser();
338
+ }
339
+ catch {
340
+ // Silent cleanup
341
+ }
342
+ this.globalBrowser = null;
343
+ }
344
+ }
345
+ /**
346
+ * Get or create the global browser session (singleton pattern)
347
+ * All sessions share this single browser instance
348
+ */
349
+ async getOrCreateGlobalBrowser(headless = false) {
350
+ // Check if existing resources are healthy before reusing
351
+ if (this.globalBrowser && this.globalTunnel) {
352
+ const [browserResult, tunnelResult] = await Promise.allSettled([
353
+ this.globalBrowser.checkHealth(),
354
+ this.globalTunnel.checkHealth(),
355
+ ]);
356
+ const browserHealthy = browserResult.status === 'fulfilled' && browserResult.value;
357
+ const tunnelHealthy = tunnelResult.status === 'fulfilled' && tunnelResult.value;
358
+ if (browserHealthy && tunnelHealthy) {
359
+ // Resources are healthy, reuse them
360
+ const localWsUrl = this.globalBrowser.getWebSocketEndpoint();
361
+ if (localWsUrl) {
362
+ const tunneledWsUrl = this.globalTunnel.getWebSocketUrl(localWsUrl);
363
+ if (tunneledWsUrl) {
364
+ await this.server.notification({
365
+ method: 'notifications/message',
366
+ params: {
367
+ level: 'info',
368
+ logger: 'global_browser',
369
+ data: {
370
+ action: 'reuse',
371
+ sessionIndex: this.globalSessionIndex,
372
+ wsUrl: tunneledWsUrl,
373
+ message: `Reusing existing browser session (index: ${this.globalSessionIndex})`,
374
+ },
375
+ },
376
+ });
377
+ return {
378
+ browser: this.globalBrowser,
379
+ tunnel: this.globalTunnel,
380
+ wsUrl: tunneledWsUrl,
381
+ sessionIndex: this.globalSessionIndex,
382
+ };
383
+ }
384
+ }
385
+ }
386
+ // Health check failed - log and cleanup stale resources
387
+ await this.server.notification({
388
+ method: 'notifications/message',
389
+ params: {
390
+ level: 'warning',
391
+ logger: 'global_browser',
392
+ data: {
393
+ action: 'health_check_failed',
394
+ browserHealthy,
395
+ tunnelHealthy,
396
+ message: 'Resources unhealthy, recreating browser and tunnel',
397
+ },
398
+ },
399
+ });
400
+ await this.cleanupGlobalResources();
401
+ }
402
+ // Create single global browser session
403
+ const apiKey = this.globalApiClient.getApiKey();
404
+ const sessionIndex = 0; // Always use index 0 for single session
405
+ await this.server.notification({
406
+ method: 'notifications/message',
407
+ params: {
408
+ level: 'info',
409
+ logger: 'global_browser',
410
+ data: {
411
+ action: 'create',
412
+ sessionIndex,
413
+ message: `Creating new global browser session (index: ${sessionIndex})`,
414
+ },
415
+ },
416
+ });
417
+ this.globalBrowser = new BrowserManager();
418
+ const browserResult = await this.globalBrowser.startBrowser({ headless });
419
+ const wsEndpoint = browserResult.wsEndpoint;
420
+ this.globalTunnel = new TunnelManager();
421
+ await this.server.notification({
422
+ method: 'notifications/message',
423
+ params: {
424
+ level: 'info',
425
+ logger: 'global_browser',
426
+ data: {
427
+ action: 'tunnel_start',
428
+ wsEndpoint,
429
+ message: `Starting global tunnel for browser WebSocket URL: ${wsEndpoint}`,
430
+ },
431
+ },
432
+ });
433
+ const wsUrl = new URL(wsEndpoint);
434
+ const browserPort = parseInt(wsUrl.port);
435
+ // Pass API key and session index 0 to tunnel for deterministic subdomain
436
+ await this.globalTunnel.startTunnel(browserPort, {
437
+ apiKey: apiKey || undefined,
438
+ sessionIndex,
439
+ });
440
+ const localWsUrl = this.globalBrowser.getWebSocketEndpoint();
441
+ if (!localWsUrl) {
442
+ await this.globalTunnel.stopTunnel();
443
+ await this.globalBrowser.stopBrowser();
444
+ this.globalBrowser = null;
445
+ this.globalTunnel = null;
446
+ throw new Error('Failed to get browser WebSocket endpoint');
447
+ }
448
+ const tunneledWsUrl = this.globalTunnel.getWebSocketUrl(localWsUrl);
449
+ if (!tunneledWsUrl) {
450
+ await this.globalTunnel.stopTunnel();
451
+ await this.globalBrowser.stopBrowser();
452
+ this.globalBrowser = null;
453
+ this.globalTunnel = null;
454
+ throw new Error('Failed to create tunneled WebSocket URL');
455
+ }
456
+ await this.server.notification({
457
+ method: 'notifications/message',
458
+ params: {
459
+ level: 'info',
460
+ logger: 'global_browser',
461
+ data: {
462
+ action: 'tunnel_established',
463
+ publicWsUrl: tunneledWsUrl,
464
+ localWsUrl: localWsUrl,
465
+ sessionIndex,
466
+ message: `Tunnel established - Public: ${tunneledWsUrl}, Local: ${localWsUrl}`,
467
+ },
468
+ },
469
+ });
470
+ this.globalSessionIndex = sessionIndex;
471
+ return {
472
+ browser: this.globalBrowser,
473
+ tunnel: this.globalTunnel,
474
+ wsUrl: tunneledWsUrl,
475
+ sessionIndex,
476
+ };
477
+ }
478
+ addBrowserSession(sessionId, browser, tunnel, sessionType, apiKey, sessionIndex) {
479
+ const session = new BrowserSession(sessionId, browser, tunnel, sessionType, this.DEFAULT_TTL_MS, apiKey, sessionIndex);
480
+ this.browserSessions.push(session);
481
+ return session;
482
+ }
483
+ async resetAllBrowserSessions() {
484
+ // Clear all session tracking
485
+ this.browserSessions = [];
486
+ // Cleanup global browser and tunnel
487
+ if (this.globalTunnel) {
488
+ try {
489
+ await this.globalTunnel.stopTunnel();
490
+ }
491
+ catch (error) {
492
+ // Silent cleanup
493
+ }
494
+ this.globalTunnel = null;
495
+ }
496
+ if (this.globalBrowser) {
497
+ try {
498
+ await this.globalBrowser.stopBrowser();
499
+ }
500
+ catch (error) {
501
+ // Silent cleanup
502
+ }
503
+ this.globalBrowser = null;
504
+ }
505
+ }
506
+ setupTools() {
507
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
508
+ return {
509
+ tools: [
510
+ {
511
+ name: 'ensure_installed',
512
+ description: 'Ensure API key is set, validate authentication, and install Playwright browsers. Does not start browsers (lazy initialization on session start).',
513
+ inputSchema: {
514
+ type: 'object',
515
+ properties: {
516
+ apiKey: {
517
+ type: 'string',
518
+ description: 'API key for desplega.ai (optional if QA_USE_API_KEY env var is set)',
519
+ },
520
+ },
521
+ required: [],
522
+ },
523
+ },
524
+ {
525
+ name: 'register_user',
526
+ description: 'Register a new user and get API key',
527
+ inputSchema: {
528
+ type: 'object',
529
+ properties: {
530
+ email: {
531
+ type: 'string',
532
+ description: 'Email address to register',
533
+ },
534
+ },
535
+ required: ['email'],
536
+ },
537
+ },
538
+ {
539
+ name: 'search_sessions',
540
+ description: 'Search and list all sessions (automated tests and development sessions) with pagination and filtering',
541
+ inputSchema: {
542
+ type: 'object',
543
+ properties: {
544
+ limit: {
545
+ type: 'number',
546
+ description: 'Maximum number of sessions to return (default: 10, min: 1)',
547
+ minimum: 1,
548
+ },
549
+ offset: {
550
+ type: 'number',
551
+ description: 'Number of sessions to skip (default: 0, min: 0)',
552
+ minimum: 0,
553
+ },
554
+ query: {
555
+ type: 'string',
556
+ description: 'Search query to filter sessions by task, URL, or status',
557
+ },
558
+ },
559
+ },
560
+ },
561
+ {
562
+ name: 'start_automated_session',
563
+ description: 'Start an automated E2E test session for QA flows and automated testing. Returns sessionId (data.agent_id) for monitoring. URL is optional - uses app config base_url if not provided.',
564
+ inputSchema: {
565
+ type: 'object',
566
+ properties: {
567
+ url: {
568
+ type: 'string',
569
+ description: 'Optional URL to test (overrides app config base_url if provided)',
570
+ },
571
+ task: {
572
+ type: 'string',
573
+ description: 'The testing task or scenario to execute',
574
+ },
575
+ dependencyId: {
576
+ type: 'string',
577
+ description: 'Optional test ID that this session depends on (must be a self test ID created by your app configuration)',
578
+ },
579
+ headless: {
580
+ type: 'boolean',
581
+ description: 'Run browser in headless mode (default: false for better visibility)',
582
+ },
583
+ },
584
+ required: ['task'],
585
+ },
586
+ },
587
+ {
588
+ name: 'start_dev_session',
589
+ description: 'Start an interactive development session for debugging and exploration. Session will not auto-pilot and allows manual browser interaction.',
590
+ inputSchema: {
591
+ type: 'object',
592
+ properties: {
593
+ url: {
594
+ type: 'string',
595
+ description: 'Optional URL to start from (overrides app config base_url if provided)',
596
+ },
597
+ task: {
598
+ type: 'string',
599
+ description: 'Description of what you want to explore or debug. Generally you can leave it blank or with a placeholder like "Waiting for user input" if you just want to start a blank session.',
600
+ },
601
+ headless: {
602
+ type: 'boolean',
603
+ description: 'Run browser in headless mode (default: false for development visibility)',
604
+ },
605
+ },
606
+ required: ['task'],
607
+ },
608
+ },
609
+ {
610
+ name: 'monitor_session',
611
+ description: 'Monitor a session status. Keep calling until status is "closed". Will alert if session needs user input, is idle, or pending.',
612
+ inputSchema: {
613
+ type: 'object',
614
+ properties: {
615
+ sessionId: {
616
+ type: 'string',
617
+ description: 'The session ID to monitor',
618
+ },
619
+ wait: {
620
+ type: 'boolean',
621
+ description: 'Wait for session to reach any non-running state (closed, idle, needs_user_input, pending) with MCP timeout protection (max 25s per call)',
622
+ },
623
+ timeout: {
624
+ type: 'number',
625
+ description: 'User timeout in seconds for wait mode (default: 60). Note: MCP timeout protection limits each call to 25s max.',
626
+ minimum: 1,
627
+ },
628
+ },
629
+ required: ['sessionId'],
630
+ },
631
+ },
632
+ {
633
+ name: 'interact_with_session',
634
+ description: 'Interact with a session - respond to questions, pause, or close the session',
635
+ inputSchema: {
636
+ type: 'object',
637
+ properties: {
638
+ sessionId: {
639
+ type: 'string',
640
+ description: 'The session ID to interact with',
641
+ },
642
+ action: {
643
+ type: 'string',
644
+ enum: ['respond', 'pause', 'close'],
645
+ description: 'Action to perform: respond (answer question), pause (stop session), or close (end session)',
646
+ },
647
+ message: {
648
+ type: 'string',
649
+ description: 'Your response message (required for "respond" action, optional for others)',
650
+ },
651
+ },
652
+ required: ['sessionId', 'action'],
653
+ },
654
+ },
655
+ {
656
+ name: 'search_automated_tests',
657
+ description: 'Search for automated tests by ID or query. If testId provided, returns detailed info for that test. Otherwise searches with optional query/pagination.',
658
+ inputSchema: {
659
+ type: 'object',
660
+ properties: {
661
+ testId: {
662
+ type: 'string',
663
+ description: 'Specific test ID to retrieve detailed information for (if provided, other params ignored)',
664
+ },
665
+ query: {
666
+ type: 'string',
667
+ description: 'Search query to filter tests by name, description, URL, or task (ignored if testId provided)',
668
+ },
669
+ limit: {
670
+ type: 'number',
671
+ description: 'Maximum number of tests to return (default: 10, min: 1) (ignored if testId provided)',
672
+ minimum: 1,
673
+ },
674
+ offset: {
675
+ type: 'number',
676
+ description: 'Number of tests to skip (default: 0, min: 0) (ignored if testId provided)',
677
+ minimum: 0,
678
+ },
679
+ self_only: {
680
+ type: 'boolean',
681
+ description: 'Filter tests by app configuration. When true, only returns tests created by your application configuration. Default: false to allow running tests from other configs locally.',
682
+ },
683
+ },
684
+ },
685
+ },
686
+ {
687
+ name: 'run_automated_tests',
688
+ description: 'Execute multiple automated tests simultaneously. Uses the global browser WebSocket URL from init_qa_server.',
689
+ inputSchema: {
690
+ type: 'object',
691
+ properties: {
692
+ test_ids: {
693
+ type: 'array',
694
+ items: {
695
+ type: 'string',
696
+ },
697
+ description: 'Array of test IDs to execute',
698
+ minItems: 1,
699
+ },
700
+ app_config_id: {
701
+ type: 'string',
702
+ description: 'Optional app config ID to run tests against (uses API key default config if not provided)',
703
+ },
704
+ },
705
+ required: ['test_ids'],
706
+ },
707
+ },
708
+ {
709
+ name: 'search_automated_test_runs',
710
+ description: 'Search automated test runs with optional filtering by test ID or run ID',
711
+ inputSchema: {
712
+ type: 'object',
713
+ properties: {
714
+ test_id: {
715
+ type: 'string',
716
+ description: 'Filter test runs by specific test ID',
717
+ },
718
+ run_id: {
719
+ type: 'string',
720
+ description: 'Filter test runs by specific run ID',
721
+ },
722
+ limit: {
723
+ type: 'number',
724
+ description: 'Maximum number of test runs to return (default: 10, min: 1)',
725
+ minimum: 1,
726
+ },
727
+ offset: {
728
+ type: 'number',
729
+ description: 'Number of test runs to skip (default: 0, min: 0)',
730
+ minimum: 0,
731
+ },
732
+ },
733
+ },
734
+ },
735
+ {
736
+ name: 'update_configuration',
737
+ description: 'Update application configuration settings including base URL, login credentials, and viewport type',
738
+ inputSchema: {
739
+ type: 'object',
740
+ properties: {
741
+ base_url: {
742
+ type: 'string',
743
+ description: 'Base URL for the application being tested',
744
+ },
745
+ login_url: {
746
+ type: 'string',
747
+ description: 'Login page URL for the application',
748
+ },
749
+ login_username: {
750
+ type: 'string',
751
+ description: 'Default username for login testing',
752
+ },
753
+ login_password: {
754
+ type: 'string',
755
+ description: 'Default password for login testing',
756
+ },
757
+ vp_type: {
758
+ type: 'string',
759
+ enum: ['big_desktop', 'desktop', 'mobile', 'tablet'],
760
+ description: 'Viewport configuration type for browser testing (default: desktop)',
761
+ },
762
+ },
763
+ },
764
+ },
765
+ {
766
+ name: 'get_configuration',
767
+ description: 'Get the current application configuration details including base URL, login settings, and viewport',
768
+ inputSchema: {
769
+ type: 'object',
770
+ properties: {},
771
+ },
772
+ },
773
+ {
774
+ name: 'reset_browser_sessions',
775
+ description: 'Reset and cleanup all active browser sessions. This will kill all browsers and tunnels. Use this when you hit the maximum session limit or need to free up resources.',
776
+ inputSchema: {
777
+ type: 'object',
778
+ properties: {},
779
+ },
780
+ },
781
+ ],
782
+ };
783
+ });
784
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
785
+ const { name, arguments: params } = request.params;
786
+ if (name === 'ensure_installed') {
787
+ return this.handleEnsureInstalled(params);
788
+ }
789
+ if (name === 'register_user') {
790
+ return this.handleRegisterUser(params);
791
+ }
792
+ if (name === 'search_sessions') {
793
+ return this.handleSearchSessions(params);
794
+ }
795
+ if (name === 'start_automated_session') {
796
+ return this.handleStartAutomatedSession(params);
797
+ }
798
+ if (name === 'start_dev_session') {
799
+ return this.handleStartDevSession(params);
800
+ }
801
+ if (name === 'monitor_session') {
802
+ return this.handleMonitorSession(params);
803
+ }
804
+ if (name === 'interact_with_session') {
805
+ return this.handleInteractWithSession(params);
806
+ }
807
+ if (name === 'search_automated_tests') {
808
+ return this.handleSearchAutomatedTests(params);
809
+ }
810
+ if (name === 'run_automated_tests') {
811
+ return this.handleRunAutomatedTests(params);
812
+ }
813
+ if (name === 'search_automated_test_runs') {
814
+ return this.handleSearchAutomatedTestRuns(params);
815
+ }
816
+ if (name === 'update_configuration') {
817
+ return this.handleUpdateConfiguration(params);
818
+ }
819
+ if (name === 'get_configuration') {
820
+ return this.handleGetConfiguration();
821
+ }
822
+ if (name === 'reset_browser_sessions') {
823
+ return this.handleResetBrowserSessions();
824
+ }
825
+ return {
826
+ content: [
827
+ {
828
+ type: 'text',
829
+ text: `Unknown tool: ${name}`,
830
+ },
831
+ ],
832
+ isError: true,
833
+ };
834
+ });
835
+ }
836
+ async handleEnsureInstalled(params) {
837
+ try {
838
+ const { apiKey } = params;
839
+ // Use provided API key or fall back to environment variable
840
+ if (apiKey) {
841
+ this.globalApiClient.setApiKey(apiKey);
842
+ }
843
+ else if (!this.globalApiClient.getApiKey()) {
844
+ return {
845
+ content: [
846
+ {
847
+ type: 'text',
848
+ text: 'No API key provided and QA_USE_API_KEY environment variable not set. Please provide an API key or set the environment variable.',
849
+ },
850
+ ],
851
+ isError: true,
852
+ };
853
+ }
854
+ const authResult = await this.globalApiClient.validateApiKey();
855
+ if (!authResult.success) {
856
+ return {
857
+ content: [
858
+ {
859
+ type: 'text',
860
+ text: `API key validation failed: ${authResult.message}`,
861
+ },
862
+ ],
863
+ isError: true,
864
+ };
865
+ }
866
+ // Always install Playwright browsers (no-op if already installed)
867
+ const browserManager = new BrowserManager();
868
+ await browserManager.installPlaywrightBrowsers();
869
+ const apiUrl = this.globalApiClient.getApiUrl();
870
+ const appUrl = ApiClient.getAppUrl();
871
+ const appConfigId = this.globalApiClient.getAppConfigId();
872
+ let message = `✅ Environment ready!\nAPI Key: Valid\nAPI URL: ${apiUrl}\nApp URL: ${appUrl}\nBrowsers: Installed`;
873
+ if (appConfigId) {
874
+ message += `\nApp Config ID: ${appConfigId}`;
875
+ }
876
+ // Include app config details if available
877
+ if (authResult.data?.app_config) {
878
+ const appConfig = authResult.data.app_config;
879
+ message += `\nApp Config: ${appConfig.name} (${appConfig.base_url})`;
880
+ if (appConfig.login_url) {
881
+ message += `\nLogin URL: ${appConfig.login_url}`;
882
+ }
883
+ // Check if app config needs setup
884
+ if (!appConfig.base_url || appConfig.base_url.trim() === '') {
885
+ message += `\n\n🔧 **Setup Required**: Your app config needs a base URL. Run update_configuration to configure.`;
886
+ }
887
+ }
888
+ else {
889
+ message += `\n\n🔧 **First Time Setup**: Run update_configuration to set your base URL and login credentials.`;
890
+ }
891
+ return {
892
+ content: [
893
+ {
894
+ type: 'text',
895
+ text: message,
896
+ },
897
+ ],
898
+ };
899
+ }
900
+ catch (error) {
901
+ return {
902
+ content: [
903
+ {
904
+ type: 'text',
905
+ text: `Environment check failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
906
+ },
907
+ ],
908
+ isError: true,
909
+ };
910
+ }
911
+ }
912
+ async handleRegisterUser(params) {
913
+ try {
914
+ const { email } = params;
915
+ const result = await this.globalApiClient.register(email);
916
+ if (result.success) {
917
+ return {
918
+ content: [
919
+ {
920
+ type: 'text',
921
+ text: `Registration successful! ${result.message}\n\nYour API key: ${result.apiKey}\n\nNow you can run: {"method": "tools/call", "params": {"name": "init_qa_server", "arguments": {"apiKey": "${result.apiKey}"}}}`,
922
+ },
923
+ ],
924
+ };
925
+ }
926
+ else {
927
+ return {
928
+ content: [
929
+ {
930
+ type: 'text',
931
+ text: `Registration failed: ${result.message}`,
932
+ },
933
+ ],
934
+ isError: true,
935
+ };
936
+ }
937
+ }
938
+ catch (error) {
939
+ return {
940
+ content: [
941
+ {
942
+ type: 'text',
943
+ text: `Registration failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
944
+ },
945
+ ],
946
+ isError: true,
947
+ };
948
+ }
949
+ }
950
+ async ensureInitialized() {
951
+ // Check for API key
952
+ if (!this.globalApiClient.getApiKey()) {
953
+ return {
954
+ content: [
955
+ {
956
+ type: 'text',
957
+ text: 'API key not configured. Please set QA_USE_API_KEY environment variable or run ensure_installed with an API key.',
958
+ },
959
+ ],
960
+ isError: true,
961
+ };
962
+ }
963
+ // Validate API key if not already validated
964
+ try {
965
+ const authResult = await this.globalApiClient.validateApiKey();
966
+ if (!authResult.success) {
967
+ return {
968
+ content: [
969
+ {
970
+ type: 'text',
971
+ text: `API key validation failed: ${authResult.message}. Please run ensure_installed with a valid API key.`,
972
+ },
973
+ ],
974
+ isError: true,
975
+ };
976
+ }
977
+ return {
978
+ content: [{ type: 'text', text: 'Initialized' }],
979
+ };
980
+ }
981
+ catch (error) {
982
+ return {
983
+ content: [
984
+ {
985
+ type: 'text',
986
+ text: `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
987
+ },
988
+ ],
989
+ isError: true,
990
+ };
991
+ }
992
+ }
993
+ async handleSearchSessions(params) {
994
+ try {
995
+ if (!this.globalApiClient.getApiKey()) {
996
+ return {
997
+ content: [
998
+ {
999
+ type: 'text',
1000
+ text: 'API key not configured. Please run init_qa_server first with an API key.',
1001
+ },
1002
+ ],
1003
+ isError: true,
1004
+ };
1005
+ }
1006
+ try {
1007
+ // Set defaults: limit=10, offset=0, query=empty
1008
+ const options = {
1009
+ limit: params.limit || 10,
1010
+ offset: params.offset || 0,
1011
+ query: params.query || '',
1012
+ };
1013
+ const sessions = await this.globalApiClient.listSessions(options);
1014
+ const sessionSummaries = sessions.map((session) => this.createSessionSummary(session));
1015
+ return {
1016
+ content: [
1017
+ {
1018
+ type: 'text',
1019
+ text: JSON.stringify({
1020
+ sessions: sessionSummaries,
1021
+ displayed: sessions.length,
1022
+ limit: options.limit,
1023
+ offset: options.offset,
1024
+ query: options.query || 'none',
1025
+ note: 'This is a summary view. Use monitor_qa_session with a specific sessionId to get full details including complete history and blocks. Use limit/offset for pagination.',
1026
+ }, null, 2),
1027
+ },
1028
+ ],
1029
+ };
1030
+ }
1031
+ catch (error) {
1032
+ return {
1033
+ content: [
1034
+ {
1035
+ type: 'text',
1036
+ text: `Failed to fetch sessions: ${error instanceof Error ? error.message : 'Unknown error'}`,
1037
+ },
1038
+ ],
1039
+ isError: true,
1040
+ };
1041
+ }
1042
+ }
1043
+ catch (error) {
1044
+ return {
1045
+ content: [
1046
+ {
1047
+ type: 'text',
1048
+ text: `Failed to list sessions: ${error instanceof Error ? error.message : 'Unknown error'}`,
1049
+ },
1050
+ ],
1051
+ isError: true,
1052
+ };
1053
+ }
1054
+ }
1055
+ async handleStartAutomatedSession(params) {
1056
+ try {
1057
+ const { url, task, dependencyId, headless = false } = params;
1058
+ // Ensure API key is set
1059
+ const initResult = await this.ensureInitialized();
1060
+ if (initResult.isError) {
1061
+ return initResult;
1062
+ }
1063
+ try {
1064
+ // Get API key for deterministic subdomain
1065
+ const apiKey = this.globalApiClient.getApiKey();
1066
+ // Use global browser session (single browser shared across all sessions)
1067
+ const { browser, tunnel, wsUrl, sessionIndex } = await this.getOrCreateGlobalBrowser(headless);
1068
+ // Create session via API with the actual wsUrl
1069
+ const { sessionId, appConfigId } = await this.globalApiClient.createSession({
1070
+ url,
1071
+ task,
1072
+ wsUrl,
1073
+ dependencyId,
1074
+ devMode: false, // Automated session
1075
+ });
1076
+ // Wrap browser and tunnel in a BrowserSession
1077
+ this.addBrowserSession(sessionId, browser, tunnel, 'automated', apiKey || undefined, sessionIndex);
1078
+ const result = {
1079
+ success: true,
1080
+ message: 'Automated test session started successfully',
1081
+ sessionId: sessionId,
1082
+ sessionType: 'automated',
1083
+ // TODO(1): For automated test sessions, use monitor_session to track progress.
1084
+ // For test runs created via run_automated_tests, use search_automated_test_runs instead.
1085
+ note: `Use sessionId "${sessionId}" for monitoring and interaction. This automated session uses App Config ID: ${appConfigId}. Monitor with monitor_session tool.`,
1086
+ };
1087
+ return {
1088
+ content: [
1089
+ {
1090
+ type: 'text',
1091
+ text: JSON.stringify(result, null, 2),
1092
+ },
1093
+ ],
1094
+ };
1095
+ }
1096
+ catch (error) {
1097
+ return {
1098
+ content: [
1099
+ {
1100
+ type: 'text',
1101
+ text: `Failed to start automated session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1102
+ },
1103
+ ],
1104
+ isError: true,
1105
+ };
1106
+ }
1107
+ }
1108
+ catch (error) {
1109
+ return {
1110
+ content: [
1111
+ {
1112
+ type: 'text',
1113
+ text: `Failed to start automated session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1114
+ },
1115
+ ],
1116
+ isError: true,
1117
+ };
1118
+ }
1119
+ }
1120
+ async handleStartDevSession(params) {
1121
+ try {
1122
+ const { url, task, headless = false } = params;
1123
+ // Ensure API key is set
1124
+ const initResult = await this.ensureInitialized();
1125
+ if (initResult.isError) {
1126
+ return initResult;
1127
+ }
1128
+ try {
1129
+ // Get API key for deterministic subdomain
1130
+ const apiKey = this.globalApiClient.getApiKey();
1131
+ // Use global browser session (single browser shared across all sessions)
1132
+ const { browser, tunnel, wsUrl, sessionIndex } = await this.getOrCreateGlobalBrowser(headless);
1133
+ // Create session via API with the actual wsUrl
1134
+ const { sessionId, appConfigId } = await this.globalApiClient.createSession({
1135
+ url,
1136
+ task,
1137
+ wsUrl,
1138
+ devMode: true, // Development session
1139
+ });
1140
+ // Wrap browser and tunnel in a BrowserSession
1141
+ this.addBrowserSession(sessionId, browser, tunnel, 'dev', apiKey || undefined, sessionIndex);
1142
+ const result = {
1143
+ success: true,
1144
+ message: 'Development session started successfully',
1145
+ sessionId: sessionId,
1146
+ sessionType: 'development',
1147
+ note: `Use sessionId "${sessionId}" for monitoring and interaction. This dev session allows manual browser control and uses App Config ID: ${appConfigId}.`,
1148
+ };
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: 'text',
1153
+ text: JSON.stringify(result, null, 2),
1154
+ },
1155
+ ],
1156
+ };
1157
+ }
1158
+ catch (error) {
1159
+ return {
1160
+ content: [
1161
+ {
1162
+ type: 'text',
1163
+ text: `Failed to start dev session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1164
+ },
1165
+ ],
1166
+ isError: true,
1167
+ };
1168
+ }
1169
+ }
1170
+ catch (error) {
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: 'text',
1175
+ text: `Failed to start dev session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1176
+ },
1177
+ ],
1178
+ isError: true,
1179
+ };
1180
+ }
1181
+ }
1182
+ async handleMonitorSession(params) {
1183
+ try {
1184
+ const { sessionId, wait = false, timeout = 60 } = params;
1185
+ if (!this.globalApiClient.getApiKey()) {
1186
+ return {
1187
+ content: [
1188
+ {
1189
+ type: 'text',
1190
+ text: 'API key not configured. Please run ensure_installed first with an API key.',
1191
+ },
1192
+ ],
1193
+ isError: true,
1194
+ };
1195
+ }
1196
+ if (wait) {
1197
+ return this.handleWaitForNonRunningState(sessionId, timeout);
1198
+ }
1199
+ try {
1200
+ const session = await this.globalApiClient.getSession(sessionId);
1201
+ const status = session.data?.status || session.status;
1202
+ // Refresh deadline if session is not closed
1203
+ if (status !== 'closed') {
1204
+ const browserSession = this.getBrowserSession(sessionId);
1205
+ if (browserSession) {
1206
+ browserSession.refreshDeadline();
1207
+ }
1208
+ }
1209
+ else {
1210
+ // Session is closed, remove from tracking (but don't cleanup global browser/tunnel)
1211
+ this.browserSessions = this.browserSessions.filter((s) => s.session_id !== sessionId);
1212
+ }
1213
+ const result = {
1214
+ sessionId: session.id,
1215
+ status: status,
1216
+ hasPendingInput: !!session.data?.pending_user_input,
1217
+ pendingInput: session.data?.pending_user_input,
1218
+ lastDone: session.data?.last_done,
1219
+ liveviewUrl: session.data?.liveview_url,
1220
+ note: 'Keep calling monitor_session until status is "closed".',
1221
+ };
1222
+ // Alert on specific statuses
1223
+ if (status === 'idle' || status === 'pending' || status === 'need_user_input') {
1224
+ let alertMessage = `⚠️ Session ${sessionId} is in "${status}" state`;
1225
+ if (status === 'need_user_input' && session.data?.pending_user_input) {
1226
+ const pendingInput = session.data.pending_user_input;
1227
+ alertMessage += `\n\n**Input Required:**\n`;
1228
+ alertMessage += `Context: ${pendingInput.reasoning || 'Session needs input'}\n`;
1229
+ alertMessage += `Question: ${pendingInput.question || 'Please provide response'}\n`;
1230
+ alertMessage += `Priority: ${pendingInput.priority || 'normal'}\n\n`;
1231
+ alertMessage += `Use interact_with_session to respond.`;
1232
+ }
1233
+ else if (status === 'idle') {
1234
+ alertMessage += `\nSession is waiting but not actively processing.`;
1235
+ }
1236
+ else if (status === 'pending') {
1237
+ alertMessage += `\nSession is pending and may need attention.`;
1238
+ }
1239
+ return {
1240
+ content: [
1241
+ {
1242
+ type: 'text',
1243
+ text: alertMessage,
1244
+ },
1245
+ ],
1246
+ isError: false,
1247
+ };
1248
+ }
1249
+ // Create conversational status with context
1250
+ const statusText = this.formatSessionProgress(session);
1251
+ return {
1252
+ content: [
1253
+ {
1254
+ type: 'text',
1255
+ text: statusText,
1256
+ },
1257
+ ],
1258
+ };
1259
+ }
1260
+ catch (error) {
1261
+ return {
1262
+ content: [
1263
+ {
1264
+ type: 'text',
1265
+ text: `Failed to monitor session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1266
+ },
1267
+ ],
1268
+ isError: true,
1269
+ };
1270
+ }
1271
+ }
1272
+ catch (error) {
1273
+ return {
1274
+ content: [
1275
+ {
1276
+ type: 'text',
1277
+ text: `Failed to monitor session: ${error instanceof Error ? error.message : 'Unknown error'}`,
1278
+ },
1279
+ ],
1280
+ isError: true,
1281
+ };
1282
+ }
1283
+ }
1284
+ async handleWaitForNonRunningState(sessionId, timeout) {
1285
+ const startTime = Date.now();
1286
+ const timeoutMs = timeout * 1000;
1287
+ let iteration = 0;
1288
+ try {
1289
+ const maxMcpTimeout = 25; // Maximum safe MCP request time (25 seconds to stay under 30s limit)
1290
+ const checkInterval = 2; // Check every 2 seconds
1291
+ const maxChecks = Math.min(Math.floor(maxMcpTimeout / checkInterval), Math.floor(timeout / checkInterval));
1292
+ for (let i = 0; i < maxChecks; i++) {
1293
+ iteration++;
1294
+ try {
1295
+ const session = await this.globalApiClient.getSession(sessionId);
1296
+ const status = session.data?.status || session.status;
1297
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1298
+ // Send progress notification
1299
+ await this.server.notification({
1300
+ method: 'notifications/message',
1301
+ params: {
1302
+ level: 'info',
1303
+ logger: 'session_monitor',
1304
+ data: {
1305
+ sessionId,
1306
+ status,
1307
+ elapsed,
1308
+ iteration,
1309
+ progress: Math.round((i / maxChecks) * 100),
1310
+ message: `Monitoring session ${sessionId} - Status: ${status} (${elapsed}s elapsed)`,
1311
+ },
1312
+ },
1313
+ });
1314
+ // Check if session is in non-running state
1315
+ if (status !== 'running') {
1316
+ let message = `Session ${sessionId} reached "${status}" state after ${elapsed} seconds`;
1317
+ if (status === 'closed') {
1318
+ message = `✅ Session ${sessionId} completed and closed after ${elapsed} seconds`;
1319
+ }
1320
+ else if (status === 'idle') {
1321
+ message = `⚠️ Session ${sessionId} is idle after ${elapsed} seconds`;
1322
+ }
1323
+ else if (status === 'need_user_input' && session.data?.pending_user_input) {
1324
+ const pendingInput = session.data.pending_user_input;
1325
+ message = `⚠️ Session ${sessionId} needs user input after ${elapsed} seconds\n\n`;
1326
+ message += `**Input Required:**\n`;
1327
+ message += `Context: ${pendingInput.reasoning || 'Session needs input'}\n`;
1328
+ message += `Question: ${pendingInput.question || 'Please provide response'}\n`;
1329
+ message += `Priority: ${pendingInput.priority || 'normal'}\n\n`;
1330
+ message += `Use interact_with_session to respond.`;
1331
+ }
1332
+ else if (status === 'pending') {
1333
+ message = `⚠️ Session ${sessionId} is pending after ${elapsed} seconds`;
1334
+ }
1335
+ return {
1336
+ content: [
1337
+ {
1338
+ type: 'text',
1339
+ text: message,
1340
+ },
1341
+ ],
1342
+ };
1343
+ }
1344
+ // Wait before next check (avoid tight loop)
1345
+ if (i < maxChecks - 1) {
1346
+ await new Promise((resolve) => setTimeout(resolve, checkInterval * 1000));
1347
+ }
1348
+ }
1349
+ catch (sessionError) {
1350
+ // If session not found or error, wait a bit and try again
1351
+ if (i < maxChecks - 1) {
1352
+ await new Promise((resolve) => setTimeout(resolve, checkInterval * 1000));
1353
+ }
1354
+ continue;
1355
+ }
1356
+ }
1357
+ // Reached max checks without completion (MCP timeout protection)
1358
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1359
+ try {
1360
+ // Get final status before returning
1361
+ const session = await this.globalApiClient.getSession(sessionId);
1362
+ const status = session.data?.status || session.status;
1363
+ // Create conversational timeout message with context
1364
+ const timeoutText = this.formatSessionProgress(session, elapsed, iteration);
1365
+ return {
1366
+ content: [
1367
+ {
1368
+ type: 'text',
1369
+ text: timeoutText,
1370
+ },
1371
+ ],
1372
+ isError: false, // Not an error, just incomplete
1373
+ };
1374
+ }
1375
+ catch (error) {
1376
+ return {
1377
+ content: [
1378
+ {
1379
+ type: 'text',
1380
+ text: JSON.stringify({
1381
+ success: false,
1382
+ message: `Session monitoring stopped after ${elapsed}s - could not get final status`,
1383
+ sessionId,
1384
+ elapsed,
1385
+ iterations: iteration,
1386
+ note: 'Call monitor_qa_session again to check current status',
1387
+ }, null, 2),
1388
+ },
1389
+ ],
1390
+ isError: true,
1391
+ };
1392
+ }
1393
+ }
1394
+ catch (error) {
1395
+ return {
1396
+ content: [
1397
+ {
1398
+ type: 'text',
1399
+ text: `Failed to wait for completion: ${error instanceof Error ? error.message : 'Unknown error'}`,
1400
+ },
1401
+ ],
1402
+ isError: true,
1403
+ };
1404
+ }
1405
+ }
1406
+ async handleInteractWithSession(params) {
1407
+ try {
1408
+ const { sessionId, action, message = '' } = params;
1409
+ if (!this.globalApiClient.getApiKey()) {
1410
+ return {
1411
+ content: [
1412
+ {
1413
+ type: 'text',
1414
+ text: '❌ API key not configured.\n\n🎯 Next step: init_qa_server({apiKey: "your-api-key"})',
1415
+ },
1416
+ ],
1417
+ isError: true,
1418
+ };
1419
+ }
1420
+ // Validate required message for respond action
1421
+ if (action === 'respond' && !message) {
1422
+ return {
1423
+ content: [
1424
+ {
1425
+ type: 'text',
1426
+ text: '❌ Message is required for "respond" action.\n\n🎯 Next step: interact_with_qa_session({sessionId: "' +
1427
+ sessionId +
1428
+ '", action: "respond", message: "your-response"})',
1429
+ },
1430
+ ],
1431
+ isError: true,
1432
+ };
1433
+ }
1434
+ try {
1435
+ // Map new action format to API format
1436
+ const apiAction = action === 'respond' ? 'response' : action;
1437
+ await this.globalApiClient.sendMessage({
1438
+ sessionId,
1439
+ action: apiAction,
1440
+ data: message,
1441
+ });
1442
+ // Refresh deadline on interaction (always)
1443
+ const browserSession = this.getBrowserSession(sessionId);
1444
+ if (browserSession) {
1445
+ browserSession.refreshDeadline();
1446
+ }
1447
+ // Create conversational output based on action
1448
+ let responseText = '';
1449
+ switch (action) {
1450
+ case 'respond':
1451
+ responseText = `✅ Response sent: "${message}"\n\n🎯 Next step: monitor_qa_session({sessionId: "${sessionId}", wait_for_completion: true})`;
1452
+ break;
1453
+ case 'pause':
1454
+ responseText = `⏸️ Session paused\n\n🎯 Next step: monitor_qa_session({sessionId: "${sessionId}"}) to check status`;
1455
+ break;
1456
+ case 'close':
1457
+ responseText = `🛑 Session closed\n\n🎯 Next step: list_qa_sessions() to see other active sessions`;
1458
+ break;
1459
+ }
1460
+ return {
1461
+ content: [
1462
+ {
1463
+ type: 'text',
1464
+ text: responseText,
1465
+ },
1466
+ ],
1467
+ };
1468
+ }
1469
+ catch (error) {
1470
+ return {
1471
+ content: [
1472
+ {
1473
+ type: 'text',
1474
+ text: `❌ Failed to ${action} session: ${error instanceof Error ? error.message : 'Unknown error'}\n\n🎯 Next step: monitor_qa_session({sessionId: "${sessionId}"}) to check session status`,
1475
+ },
1476
+ ],
1477
+ isError: true,
1478
+ };
1479
+ }
1480
+ }
1481
+ catch (error) {
1482
+ return {
1483
+ content: [
1484
+ {
1485
+ type: 'text',
1486
+ text: `❌ Failed to interact with session: ${error instanceof Error ? error.message : 'Unknown error'}\n\n🎯 Next step: list_qa_sessions() to verify session exists`,
1487
+ },
1488
+ ],
1489
+ isError: true,
1490
+ };
1491
+ }
1492
+ }
1493
+ async handleSearchAutomatedTests(params) {
1494
+ try {
1495
+ if (!this.globalApiClient.getApiKey()) {
1496
+ return {
1497
+ content: [
1498
+ {
1499
+ type: 'text',
1500
+ text: '❌ API key not configured.\n\n🎯 Next step: init_qa_server({apiKey: "your-api-key"})',
1501
+ },
1502
+ ],
1503
+ isError: true,
1504
+ };
1505
+ }
1506
+ const { testId, query, limit = 10, offset = 0, self_only = false } = params;
1507
+ try {
1508
+ // If testId provided, get specific test details
1509
+ if (testId) {
1510
+ const test = await this.globalApiClient.getTest(testId);
1511
+ return {
1512
+ content: [
1513
+ {
1514
+ type: 'text',
1515
+ text: `🔍 **Test Found:** ${test.name || testId}\n\n📋 **Details:**\n${JSON.stringify(test, null, 2)}\n\n🎯 Next step: run_automated_tests({test_ids: ["${testId}"]}) to execute this test`,
1516
+ },
1517
+ ],
1518
+ };
1519
+ }
1520
+ // Otherwise, search for tests
1521
+ const options = { limit, offset, query: query || '', self_only };
1522
+ const tests = await this.globalApiClient.listTests(options);
1523
+ const testSummaries = tests.map((test) => this.createTestSummary(test));
1524
+ if (tests.length === 0) {
1525
+ return {
1526
+ content: [
1527
+ {
1528
+ type: 'text',
1529
+ text: query
1530
+ ? `🔍 No tests found matching "${query}"\n\n🎯 Next step: find_automated_test() to see all available tests`
1531
+ : '📋 No automated tests found\n\n🎯 Next step: Create tests in your desplega.ai dashboard',
1532
+ },
1533
+ ],
1534
+ };
1535
+ }
1536
+ if (tests.length === 1) {
1537
+ const singleTest = tests[0];
1538
+ return {
1539
+ content: [
1540
+ {
1541
+ type: 'text',
1542
+ text: `🎯 **Found 1 test:** ${singleTest.name || singleTest.id}\n\n📋 **Summary:**\n${JSON.stringify(testSummaries[0], null, 2)}\n\n🎯 Next steps:\n• find_automated_test({testId: "${singleTest.id}"}) for full details\n• run_automated_tests({test_ids: ["${singleTest.id}"]}) to execute`,
1543
+ },
1544
+ ],
1545
+ };
1546
+ }
1547
+ return {
1548
+ content: [
1549
+ {
1550
+ type: 'text',
1551
+ text: `🔍 **Found ${tests.length} tests** ${query ? `matching "${query}"` : ''}\n\n📋 **Tests:**\n${JSON.stringify(testSummaries, null, 2)}\n\n🎯 Next steps:\n• find_automated_test({testId: "specific-id"}) for details on any test\n• run_automated_tests({test_ids: ["id1", "id2"]}) to execute multiple tests`,
1552
+ },
1553
+ ],
1554
+ };
1555
+ }
1556
+ catch (error) {
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: 'text',
1561
+ text: `❌ Failed to find tests: ${error instanceof Error ? error.message : 'Unknown error'}\n\n🎯 Next step: Verify your API key and try again`,
1562
+ },
1563
+ ],
1564
+ isError: true,
1565
+ };
1566
+ }
1567
+ }
1568
+ catch (error) {
1569
+ return {
1570
+ content: [
1571
+ {
1572
+ type: 'text',
1573
+ text: `❌ Failed to find tests: ${error instanceof Error ? error.message : 'Unknown error'}\n\n🎯 Next step: Check your connection and try again`,
1574
+ },
1575
+ ],
1576
+ isError: true,
1577
+ };
1578
+ }
1579
+ }
1580
+ async handleRunAutomatedTests(params) {
1581
+ try {
1582
+ const { test_ids, app_config_id } = params;
1583
+ if (!this.globalApiClient.getApiKey()) {
1584
+ return {
1585
+ content: [
1586
+ {
1587
+ type: 'text',
1588
+ text: 'API key not configured. Please run init_qa_server first with an API key.',
1589
+ },
1590
+ ],
1591
+ isError: true,
1592
+ };
1593
+ }
1594
+ try {
1595
+ // Get API key for deterministic subdomain
1596
+ const apiKey = this.globalApiClient.getApiKey();
1597
+ // Use global browser session (single browser shared across all sessions)
1598
+ const { browser, tunnel, wsUrl, sessionIndex } = await this.getOrCreateGlobalBrowser(true); // headless for test runs
1599
+ // Run tests with the wsUrl
1600
+ const result = await this.globalApiClient.runTests({
1601
+ test_ids,
1602
+ ws_url: wsUrl,
1603
+ app_config_id,
1604
+ });
1605
+ // For test runs, we store the browser/tunnel with a generic test_run session type
1606
+ // We'll use the test_run_id as the session identifier
1607
+ if (result.success && result.test_run_id) {
1608
+ this.addBrowserSession(result.test_run_id, browser, tunnel, 'test_run', apiKey || undefined, sessionIndex);
1609
+ }
1610
+ if (result.success) {
1611
+ return {
1612
+ content: [
1613
+ {
1614
+ type: 'text',
1615
+ text: JSON.stringify({
1616
+ success: true,
1617
+ message: `✅ Successfully started ${test_ids.length} automated tests`,
1618
+ test_run_id: result.test_run_id,
1619
+ test_ids: test_ids,
1620
+ app_config_id: app_config_id || 'Using API key default config',
1621
+ sessions: result.sessions,
1622
+ note: 'Tests are now running. Use search_automated_test_runs with the test_run_id or test_id to monitor progress.',
1623
+ }, null, 2),
1624
+ },
1625
+ ],
1626
+ };
1627
+ }
1628
+ else {
1629
+ // NOTE: Do NOT cleanup browser/tunnel on failure since they are shared globally
1630
+ return {
1631
+ content: [
1632
+ {
1633
+ type: 'text',
1634
+ text: JSON.stringify({
1635
+ success: false,
1636
+ message: result.message || 'Failed to start tests',
1637
+ test_ids: test_ids,
1638
+ }, null, 2),
1639
+ },
1640
+ ],
1641
+ isError: true,
1642
+ };
1643
+ }
1644
+ }
1645
+ catch (error) {
1646
+ return {
1647
+ content: [
1648
+ {
1649
+ type: 'text',
1650
+ text: `Failed to run tests: ${error instanceof Error ? error.message : 'Unknown error'}`,
1651
+ },
1652
+ ],
1653
+ isError: true,
1654
+ };
1655
+ }
1656
+ }
1657
+ catch (error) {
1658
+ return {
1659
+ content: [
1660
+ {
1661
+ type: 'text',
1662
+ text: `Failed to run tests: ${error instanceof Error ? error.message : 'Unknown error'}`,
1663
+ },
1664
+ ],
1665
+ isError: true,
1666
+ };
1667
+ }
1668
+ }
1669
+ async handleSearchAutomatedTestRuns(params) {
1670
+ try {
1671
+ if (!this.globalApiClient.getApiKey()) {
1672
+ return {
1673
+ content: [
1674
+ {
1675
+ type: 'text',
1676
+ text: 'API key not configured. Please run init_qa_server first with an API key.',
1677
+ },
1678
+ ],
1679
+ isError: true,
1680
+ };
1681
+ }
1682
+ try {
1683
+ // Set defaults: limit=10, offset=0
1684
+ const options = {
1685
+ test_id: params.test_id,
1686
+ run_id: params.run_id,
1687
+ limit: params.limit || 10,
1688
+ offset: params.offset || 0,
1689
+ };
1690
+ const testRuns = await this.globalApiClient.listTestRuns(options);
1691
+ return {
1692
+ content: [
1693
+ {
1694
+ type: 'text',
1695
+ text: JSON.stringify({
1696
+ test_runs: testRuns,
1697
+ displayed: testRuns.length,
1698
+ limit: options.limit,
1699
+ offset: options.offset,
1700
+ filters: {
1701
+ test_id: options.test_id || 'none',
1702
+ run_id: options.run_id || 'none',
1703
+ },
1704
+ note: 'Test runs with execution details, status, and performance metrics. Use limit/offset for pagination.',
1705
+ }, null, 2),
1706
+ },
1707
+ ],
1708
+ };
1709
+ }
1710
+ catch (error) {
1711
+ return {
1712
+ content: [
1713
+ {
1714
+ type: 'text',
1715
+ text: `Failed to fetch test runs: ${error instanceof Error ? error.message : 'Unknown error'}`,
1716
+ },
1717
+ ],
1718
+ isError: true,
1719
+ };
1720
+ }
1721
+ }
1722
+ catch (error) {
1723
+ return {
1724
+ content: [
1725
+ {
1726
+ type: 'text',
1727
+ text: `Failed to list test runs: ${error instanceof Error ? error.message : 'Unknown error'}`,
1728
+ },
1729
+ ],
1730
+ isError: true,
1731
+ };
1732
+ }
1733
+ }
1734
+ async handleUpdateConfiguration(params) {
1735
+ try {
1736
+ if (!this.globalApiClient.getApiKey()) {
1737
+ return {
1738
+ content: [
1739
+ {
1740
+ type: 'text',
1741
+ text: 'API key not configured. Please run init_qa_server first with an API key.',
1742
+ },
1743
+ ],
1744
+ isError: true,
1745
+ };
1746
+ }
1747
+ try {
1748
+ const result = await this.globalApiClient.updateAppConfig(params);
1749
+ if (result.success) {
1750
+ const appConfigId = this.globalApiClient.getAppConfigId();
1751
+ let successMessage = `✅ App config updated successfully! ${result.message}`;
1752
+ if (appConfigId) {
1753
+ successMessage += `\nApp Config ID: ${appConfigId}`;
1754
+ }
1755
+ // List the changes made
1756
+ const changes = Object.entries(params)
1757
+ .filter(([_, value]) => value !== undefined)
1758
+ .map(([key, value]) => ` - ${key}: ${value}`)
1759
+ .join('\n');
1760
+ if (changes) {
1761
+ successMessage += `\n\nUpdated settings:\n${changes}`;
1762
+ }
1763
+ successMessage +=
1764
+ '\n\n💡 **Tip:** These settings will be used for future QA sessions. You may need to restart active sessions for changes to take effect.';
1765
+ return {
1766
+ content: [
1767
+ {
1768
+ type: 'text',
1769
+ text: successMessage,
1770
+ },
1771
+ ],
1772
+ };
1773
+ }
1774
+ else {
1775
+ return {
1776
+ content: [
1777
+ {
1778
+ type: 'text',
1779
+ text: `❌ Failed to update app config: ${result.message}`,
1780
+ },
1781
+ ],
1782
+ isError: true,
1783
+ };
1784
+ }
1785
+ }
1786
+ catch (error) {
1787
+ return {
1788
+ content: [
1789
+ {
1790
+ type: 'text',
1791
+ text: `Failed to update app config: ${error instanceof Error ? error.message : 'Unknown error'}`,
1792
+ },
1793
+ ],
1794
+ isError: true,
1795
+ };
1796
+ }
1797
+ }
1798
+ catch (error) {
1799
+ return {
1800
+ content: [
1801
+ {
1802
+ type: 'text',
1803
+ text: `Failed to update app config: ${error instanceof Error ? error.message : 'Unknown error'}`,
1804
+ },
1805
+ ],
1806
+ isError: true,
1807
+ };
1808
+ }
1809
+ }
1810
+ async handleGetConfiguration() {
1811
+ try {
1812
+ if (!this.globalApiClient.getApiKey()) {
1813
+ return {
1814
+ content: [
1815
+ {
1816
+ type: 'text',
1817
+ text: 'API key not configured. Please run init_qa_server first with an API key.',
1818
+ },
1819
+ ],
1820
+ isError: true,
1821
+ };
1822
+ }
1823
+ try {
1824
+ const checkData = await this.globalApiClient.validateApiKey();
1825
+ return {
1826
+ content: [
1827
+ {
1828
+ type: 'text',
1829
+ text: JSON.stringify(checkData, null, 2),
1830
+ },
1831
+ ],
1832
+ };
1833
+ }
1834
+ catch (error) {
1835
+ return {
1836
+ content: [
1837
+ {
1838
+ type: 'text',
1839
+ text: `Failed to get current app config: ${error instanceof Error ? error.message : 'Unknown error'}`,
1840
+ },
1841
+ ],
1842
+ isError: true,
1843
+ };
1844
+ }
1845
+ }
1846
+ catch (error) {
1847
+ return {
1848
+ content: [
1849
+ {
1850
+ type: 'text',
1851
+ text: `Failed to get current app config: ${error instanceof Error ? error.message : 'Unknown error'}`,
1852
+ },
1853
+ ],
1854
+ isError: true,
1855
+ };
1856
+ }
1857
+ }
1858
+ async handleResetBrowserSessions() {
1859
+ try {
1860
+ const sessionCount = this.browserSessions.length;
1861
+ await this.resetAllBrowserSessions();
1862
+ return {
1863
+ content: [
1864
+ {
1865
+ type: 'text',
1866
+ text: `✅ Successfully reset ${sessionCount} browser session(s). All browsers and tunnels have been cleaned up.`,
1867
+ },
1868
+ ],
1869
+ };
1870
+ }
1871
+ catch (error) {
1872
+ return {
1873
+ content: [
1874
+ {
1875
+ type: 'text',
1876
+ text: `Failed to reset browser sessions: ${error instanceof Error ? error.message : 'Unknown error'}`,
1877
+ },
1878
+ ],
1879
+ isError: true,
1880
+ };
1881
+ }
1882
+ }
1883
+ setupResources() {
1884
+ // List resources handler
1885
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
1886
+ return {
1887
+ resources: [
1888
+ {
1889
+ uri: 'qa-use://guides/getting-started',
1890
+ name: 'Getting Started Guide',
1891
+ description: 'Complete guide to setting up and using QA-Use MCP server',
1892
+ mimeType: 'text/markdown',
1893
+ },
1894
+ {
1895
+ uri: 'qa-use://guides/workflows',
1896
+ name: 'Testing Workflows',
1897
+ description: 'Common testing workflows and best practices',
1898
+ mimeType: 'text/markdown',
1899
+ },
1900
+ {
1901
+ uri: 'qa-use://guides/tools',
1902
+ name: 'Tool Reference',
1903
+ description: 'Detailed documentation for all available MCP tools',
1904
+ mimeType: 'text/markdown',
1905
+ },
1906
+ ],
1907
+ };
1908
+ });
1909
+ // Read resource handler
1910
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1911
+ const { uri } = request.params;
1912
+ switch (uri) {
1913
+ case 'qa-use://guides/getting-started':
1914
+ return {
1915
+ contents: [
1916
+ {
1917
+ uri,
1918
+ mimeType: 'text/markdown',
1919
+ text: this.getGettingStartedGuide(),
1920
+ },
1921
+ ],
1922
+ };
1923
+ case 'qa-use://guides/workflows':
1924
+ return {
1925
+ contents: [
1926
+ {
1927
+ uri,
1928
+ mimeType: 'text/markdown',
1929
+ text: this.getWorkflowsGuide(),
1930
+ },
1931
+ ],
1932
+ };
1933
+ case 'qa-use://guides/tools':
1934
+ return {
1935
+ contents: [
1936
+ {
1937
+ uri,
1938
+ mimeType: 'text/markdown',
1939
+ text: this.getToolsGuide(),
1940
+ },
1941
+ ],
1942
+ };
1943
+ default:
1944
+ throw new Error(`Resource not found: ${uri}`);
1945
+ }
1946
+ });
1947
+ }
1948
+ setupPrompts() {
1949
+ // List prompts handler
1950
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
1951
+ return {
1952
+ prompts: [
1953
+ {
1954
+ name: 'aaa_test',
1955
+ description: 'Generate a structured test scenario using the AAA (Arrange-Act-Assert) framework',
1956
+ arguments: [
1957
+ {
1958
+ name: 'test_type',
1959
+ description: 'Type of test (login, form, navigation, e-commerce, accessibility, etc.)',
1960
+ required: true,
1961
+ },
1962
+ {
1963
+ name: 'url',
1964
+ description: 'Target URL for testing',
1965
+ required: true,
1966
+ },
1967
+ {
1968
+ name: 'feature',
1969
+ description: 'Specific feature or functionality to test',
1970
+ required: false,
1971
+ },
1972
+ {
1973
+ name: 'expected_outcome',
1974
+ description: 'Expected outcome or success criteria',
1975
+ required: false,
1976
+ },
1977
+ ],
1978
+ },
1979
+ ],
1980
+ };
1981
+ });
1982
+ // Get prompt handler
1983
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1984
+ const { name, arguments: args } = request.params;
1985
+ switch (name) {
1986
+ case 'aaa_test':
1987
+ return {
1988
+ description: 'AAA framework test',
1989
+ messages: [
1990
+ {
1991
+ role: 'user',
1992
+ content: {
1993
+ type: 'text',
1994
+ text: this.generateAAATestPrompt(args),
1995
+ },
1996
+ },
1997
+ ],
1998
+ };
1999
+ default:
2000
+ throw new Error(`Prompt not found: ${name}`);
2001
+ }
2002
+ });
2003
+ }
2004
+ generateAAATestPrompt(args = {}) {
2005
+ const testType = String(args.test_type || 'general');
2006
+ const url = String(args.url || 'https://example.com');
2007
+ const feature = String(args.feature || 'core functionality');
2008
+ const expectedOutcome = String(args.expected_outcome || 'successful completion');
2009
+ return `Create a ${testType} test using the AAA (Arrange-Act-Assert) framework for QA-Use MCP server.
2010
+
2011
+ **Test Type:** ${testType}
2012
+ **Target URL:** ${url}
2013
+ **Feature:** ${feature}
2014
+ **Expected Outcome:** ${expectedOutcome}
2015
+
2016
+ **ARRANGE:**
2017
+ Navigate to ${url} and prepare the testing environment. Verify the page loads correctly and the "${feature}" is visible and accessible. Set up any necessary preconditions for testing.
2018
+
2019
+ **ACT:**
2020
+ Execute the primary ${testType} workflow for "${feature}". Interact with the feature following the expected user flow.
2021
+
2022
+ **ASSERT:**
2023
+ Verify that the expected outcome is achieved: ${expectedOutcome}
2024
+ - Check that the feature works as expected
2025
+ - Verify no errors are displayed
2026
+ - Confirm the UI state is correct
2027
+ - Validate any expected changes (URL, content, state)
2028
+
2029
+ **QA-Use Implementation Tips:**
2030
+ - Use specific, descriptive selectors (button text, labels, headings)
2031
+ - Wait for page loads and state changes before acting
2032
+ - Verify multiple aspects: functionality, UI state, and user feedback
2033
+ - Handle async operations and loading states appropriately
2034
+ - Test edge cases and error conditions
2035
+
2036
+ **Success Criteria:**
2037
+ - The ${testType} test completes successfully
2038
+ - All assertions pass
2039
+ - The feature "${feature}" works as expected
2040
+ - Expected outcome "${expectedOutcome}" is achieved
2041
+
2042
+ Write the task description for start_automated_session following this AAA structure.`;
2043
+ }
2044
+ getGettingStartedGuide() {
2045
+ return `# QA-Use MCP Server - Getting Started Guide
2046
+
2047
+ ## Quick Setup
2048
+
2049
+ ### 1. Initialize the Server
2050
+ \`\`\`json
2051
+ {
2052
+ "tool": "ensure_installed",
2053
+ "params": {
2054
+ "apiKey": "your-api-key"
2055
+ }
2056
+ }
2057
+ \`\`\`
2058
+
2059
+ ### 2. Configure Your App (One-time setup)
2060
+ \`\`\`json
2061
+ {
2062
+ "tool": "update_configuration",
2063
+ "params": {
2064
+ "base_url": "https://your-app.com",
2065
+ "login_url": "https://your-app.com/login",
2066
+ "login_username": "test@example.com",
2067
+ "login_password": "your-password",
2068
+ "vp_type": "desktop"
2069
+ }
2070
+ }
2071
+ \`\`\`
2072
+
2073
+ ### 3. Start Testing (URL optional - uses app config)
2074
+ \`\`\`json
2075
+ {
2076
+ "tool": "start_automated_session",
2077
+ "params": {
2078
+ "task": "Test login functionality using configured credentials"
2079
+ }
2080
+ }
2081
+ \`\`\`
2082
+
2083
+ ### 4. Monitor Progress
2084
+ \`\`\`json
2085
+ {
2086
+ "tool": "monitor_session",
2087
+ "params": {
2088
+ "sessionId": "session-id-from-start-response",
2089
+ "wait": true,
2090
+ "timeout": 120
2091
+ }
2092
+ }
2093
+ \`\`\`
2094
+
2095
+ ## Workflow Benefits
2096
+
2097
+ - **One-time Setup**: Configure your app once, test repeatedly
2098
+ - **User Isolation**: Each user has their own app config
2099
+ - **Simplified Testing**: No need to pass URL/credentials every time
2100
+ - **Flexible Overrides**: Can still specify URL for specific tests
2101
+ - **Session Types**: Automated (hands-off) or Dev (interactive)
2102
+
2103
+ ## Core Concepts
2104
+
2105
+ - **App Configuration**: Centralized configuration for your testing environment
2106
+ - **Automated Sessions**: QA testing that runs without user interaction
2107
+ - **Dev Sessions**: Interactive sessions for manual testing and debugging
2108
+ - **Monitoring**: Real-time session progress tracking
2109
+ - **Batch Testing**: Execute multiple automated tests simultaneously
2110
+
2111
+ ## User Journey
2112
+
2113
+ 1. **Register** → Get API key with \`register_user\` (if needed)
2114
+ 2. **Initialize** → \`ensure_installed\` with API key
2115
+ 3. **Configure** → \`update_configuration\` with your app settings
2116
+ 4. **Test** → \`start_automated_session\` or \`start_dev_session\`
2117
+ 5. **Monitor** → \`monitor_session\` to track progress
2118
+ 6. **Batch** → \`run_automated_tests\` to run multiple tests
2119
+
2120
+ ## Next Steps
2121
+
2122
+ 1. Read the Workflows guide for testing patterns
2123
+ 2. Check out the Tools Reference for detailed documentation
2124
+ 3. Use the AAA prompt template for structured test scenarios
2125
+ 4. Use \`get_configuration\` to view your current setup
2126
+ `;
2127
+ }
2128
+ getWorkflowsGuide() {
2129
+ return `# QA-Use Testing Workflows
2130
+
2131
+ ## Workflow 1: Automated Testing
2132
+ *Recommended for repeatable QA tests*
2133
+
2134
+ 1. **Initialize** → \`ensure_installed\` with API key
2135
+ 2. **Configure** → \`update_configuration\` (one-time setup)
2136
+ 3. **Test** → \`start_automated_session\` (URL optional)
2137
+ 4. **Monitor** → \`monitor_session\` with wait=true
2138
+ 5. **Explore** → \`get_configuration\` to see your setup
2139
+
2140
+ ### App Config Setup Example
2141
+ \`\`\`json
2142
+ {
2143
+ "tool": "update_app_config",
2144
+ "params": {
2145
+ "base_url": "https://staging.myapp.com",
2146
+ "login_url": "https://staging.myapp.com/auth/login",
2147
+ "login_username": "tester@mycompany.com",
2148
+ "login_password": "secure-test-password",
2149
+ "vp_type": "desktop"
2150
+ }
2151
+ }
2152
+ \`\`\`
2153
+
2154
+ ### Testing with App Config
2155
+ \`\`\`json
2156
+ {
2157
+ "tool": "start_qa_session",
2158
+ "params": {
2159
+ "task": "Test user registration flow"
2160
+ }
2161
+ }
2162
+ \`\`\`
2163
+
2164
+ ## Workflow 1: Interactive Testing
2165
+ *For manual testing with AI assistance*
2166
+
2167
+ 1. **Initialize** → \`init_qa_server\`
2168
+ 2. **Configure** → \`update_app_config\` (if first time)
2169
+ 3. **Start Session** → \`start_qa_session\` (URL optional)
2170
+ 4. **Monitor & Interact** → \`monitor_qa_session\` + \`interact_with_qa_session\`
2171
+ 5. **Completion** → Session reaches "closed" or "idle"
2172
+
2173
+ ## Workflow 2: Automated Batch Testing
2174
+ *For running pre-defined test suites*
2175
+
2176
+ 1. **Initialize** → \`init_qa_server\`
2177
+ 2. **Find Tests** → \`find_automated_test\`
2178
+ 3. **Run Tests** → \`run_automated_tests\` (with optional app_config_id)
2179
+ 4. **Monitor Progress** → \`list_qa_sessions\` + \`monitor_qa_session\`
2180
+
2181
+ ### Multi-Environment Testing
2182
+ \`\`\`json
2183
+ {
2184
+ "tool": "run_automated_tests",
2185
+ "params": {
2186
+ "test_ids": ["login-test", "checkout-test"],
2187
+ "app_config_id": "staging-config-id"
2188
+ }
2189
+ }
2190
+ \`\`\`
2191
+
2192
+ ## Workflow 3: Test Development
2193
+ *For creating and refining test cases*
2194
+
2195
+ 1. **Find Tests** → \`find_automated_test\` (by query or testId)
2196
+ 2. **Start New Session** → \`start_qa_session\` (uses app config + pass dependencyId if needed)
2197
+ 3. **Monitor & Iterate** → \`monitor_qa_session\`
2198
+
2199
+ ## Best Practices
2200
+
2201
+ ### App Configuration
2202
+ - **One-time Setup**: Configure your app once per environment
2203
+ - **User Isolation**: Each team member has their own app config
2204
+ - **Environment Management**: Use \`list_app_configs\` to see available configs
2205
+ - **Override When Needed**: Still pass URL for specific page testing
2206
+
2207
+ ### Session Management
2208
+ - Always use \`wait_for_completion=true\` for unattended monitoring
2209
+ - Set appropriate timeouts (default: 60s, recommended: 120-300s for complex tests)
2210
+ - Use pagination (\`limit\`/\`offset\`) when listing many sessions
2211
+
2212
+ ### Error Handling
2213
+ - Check session status regularly: "active", "pending", "closed", "idle"
2214
+ - Handle user input requests promptly
2215
+ - Monitor for timeout conditions
2216
+
2217
+ ### Performance
2218
+ - Use batch testing for multiple related tests
2219
+ - Leverage global WebSocket URL for consistent browser access
2220
+ - Implement proper cleanup with session monitoring
2221
+ - Use \`app_config_id\` to test against different environments efficiently
2222
+ `;
2223
+ }
2224
+ getToolsGuide() {
2225
+ return `# MCP Tools Reference
2226
+
2227
+ This guide provides detailed documentation for all available QA-Use MCP tools.
2228
+
2229
+ ## Setup & Configuration
2230
+
2231
+ ### ensure_installed
2232
+ Ensure API key is set, validate authentication, and install Playwright browsers.
2233
+ - **Parameters**: apiKey (optional)
2234
+ - **Usage**: Run this first to set up your environment
2235
+
2236
+ ### register_user
2237
+ Register a new user account with desplega.ai and receive an API key.
2238
+ - **Parameters**: email (required)
2239
+ - **Usage**: For new users who need an API key
2240
+
2241
+ ### update_configuration
2242
+ Update application configuration settings including base URL, login credentials, and viewport type.
2243
+ - **Parameters**: base_url, login_url, login_username, login_password, vp_type (all optional)
2244
+ - **Usage**: One-time setup for your application under test
2245
+
2246
+ ### get_configuration
2247
+ Get the current application configuration details.
2248
+ - **Parameters**: None
2249
+ - **Usage**: View your current app configuration
2250
+
2251
+ ## Session Management
2252
+
2253
+ ### search_sessions
2254
+ Search and list all sessions (automated tests and development sessions) with pagination and filtering.
2255
+ - **Parameters**: limit, offset, query (all optional)
2256
+ - **Usage**: View all your testing sessions
2257
+
2258
+ ### start_automated_session
2259
+ Start an automated E2E test session for QA flows. Returns sessionId for monitoring.
2260
+ - **Parameters**: task (required), url, dependencyId, headless (optional)
2261
+ - **Usage**: Run automated tests that execute without user interaction
2262
+
2263
+ ### start_dev_session
2264
+ Start an interactive development session for debugging and exploration.
2265
+ - **Parameters**: task (required), url, headless (optional)
2266
+ - **Usage**: Manual testing and debugging with browser control
2267
+
2268
+ ### monitor_session
2269
+ Monitor a session status. Keep calling until status is "closed".
2270
+ - **Parameters**: sessionId (required), wait, timeout (optional)
2271
+ - **Usage**: Track test execution progress
2272
+
2273
+ ### interact_with_session
2274
+ Interact with a session - respond to questions, pause, or close.
2275
+ - **Parameters**: sessionId, action (required), message (optional)
2276
+ - **Actions**: respond, pause, close
2277
+ - **Usage**: Provide input when session asks questions
2278
+
2279
+ ## Test Management
2280
+
2281
+ ### search_automated_tests
2282
+ Search for automated tests by ID or query.
2283
+ - **Parameters**: testId, query, limit, offset (all optional)
2284
+ - **Usage**: Find existing automated tests
2285
+
2286
+ ### run_automated_tests
2287
+ Execute multiple automated tests simultaneously.
2288
+ - **Parameters**: test_ids (required), app_config_id, ws_url (optional)
2289
+ - **Usage**: Run batch tests in parallel
2290
+
2291
+ ### search_automated_test_runs
2292
+ Search automated test runs with optional filtering.
2293
+ - **Parameters**: test_id, run_id, limit, offset (all optional)
2294
+ - **Usage**: View test execution history and results
2295
+
2296
+ ## Common Usage Patterns
2297
+
2298
+ ### Pattern 1: First-Time Setup
2299
+ \`\`\`
2300
+ 1. ensure_installed
2301
+ 2. update_configuration (set base_url, login credentials)
2302
+ 3. start_automated_session (begin testing)
2303
+ 4. monitor_session (track progress)
2304
+ \`\`\`
2305
+
2306
+ ### Pattern 2: Development Testing
2307
+ \`\`\`
2308
+ 1. start_dev_session (with task and url)
2309
+ 2. monitor_session (watch execution)
2310
+ 3. interact_with_session (provide input if needed)
2311
+ \`\`\`
2312
+
2313
+ ### Pattern 3: Batch Testing
2314
+ \`\`\`
2315
+ 1. search_automated_tests (find tests to run)
2316
+ 2. run_automated_tests (execute multiple tests)
2317
+ 3. search_sessions (monitor all running sessions)
2318
+ 4. search_automated_test_runs (view results)
2319
+ \`\`\`
2320
+
2321
+ ## Session Status Guide
2322
+
2323
+ | Status | Meaning | Next Action |
2324
+ |--------|---------|------------|
2325
+ | running | Test executing | Continue monitoring |
2326
+ | needs_user_input | Waiting for input | Use interact_with_session |
2327
+ | closed | Completed | Review results |
2328
+ | idle | Paused | Check status or close |
2329
+ | pending | Queued | Wait or monitor |
2330
+
2331
+ ## Best Practices
2332
+
2333
+ 1. **Always set up configuration first** using update_configuration
2334
+ 2. **Use wait=true** for monitor_session in automated workflows
2335
+ 3. **Set appropriate timeouts** based on test complexity
2336
+ 4. **Handle user input promptly** when sessions need it
2337
+ 5. **Use dev sessions** for exploration and debugging
2338
+ 6. **Use automated sessions** for repeatable QA tests
2339
+ 7. **Search test runs** to analyze test history and flakiness
2340
+
2341
+ For more detailed examples, see the Getting Started and Workflows guides.
2342
+ `;
2343
+ }
2344
+ getServer() {
2345
+ return this.server;
2346
+ }
2347
+ setApiKey(apiKey) {
2348
+ this.globalApiClient.setApiKey(apiKey);
2349
+ }
2350
+ async start() {
2351
+ const transport = new StdioServerTransport();
2352
+ await this.server.connect(transport);
2353
+ // console.debug(`${getName()} running (PID: ${process.pid}, Version: ${getVersion()})`);
2354
+ // Handle graceful shutdown
2355
+ process.on('SIGINT', async () => {
2356
+ await this.cleanup();
2357
+ process.exit(0);
2358
+ });
2359
+ process.on('SIGTERM', async () => {
2360
+ await this.cleanup();
2361
+ process.exit(0);
2362
+ });
2363
+ }
2364
+ async cleanup() {
2365
+ // Clear cleanup interval
2366
+ if (this.cleanupInterval) {
2367
+ clearInterval(this.cleanupInterval);
2368
+ this.cleanupInterval = null;
2369
+ }
2370
+ // Cleanup all browser sessions (includes global browser/tunnel)
2371
+ await this.resetAllBrowserSessions();
2372
+ }
2373
+ }
2374
+ // Export the server class for use in different transport modes
2375
+ export { QAUseMcpServer };
2376
+ //# sourceMappingURL=server.js.map