@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.
- package/LICENSE +21 -0
- package/README.md +1003 -0
- package/bin/qa-use.js +7 -0
- package/dist/lib/api/index.d.ts +296 -0
- package/dist/lib/api/index.d.ts.map +1 -0
- package/dist/lib/api/index.js +564 -0
- package/dist/lib/api/index.js.map +1 -0
- package/dist/lib/api/sse.d.ts +33 -0
- package/dist/lib/api/sse.d.ts.map +1 -0
- package/dist/lib/api/sse.js +97 -0
- package/dist/lib/api/sse.js.map +1 -0
- package/dist/lib/browser/index.d.ts +28 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +145 -0
- package/dist/lib/browser/index.js.map +1 -0
- package/dist/lib/env/index.d.ts +41 -0
- package/dist/lib/env/index.d.ts.map +1 -0
- package/dist/lib/env/index.js +125 -0
- package/dist/lib/env/index.js.map +1 -0
- package/dist/lib/tunnel/index.d.ts +38 -0
- package/dist/lib/tunnel/index.d.ts.map +1 -0
- package/dist/lib/tunnel/index.js +154 -0
- package/dist/lib/tunnel/index.js.map +1 -0
- package/dist/package.json +100 -0
- package/dist/src/cli/commands/info.d.ts +6 -0
- package/dist/src/cli/commands/info.d.ts.map +1 -0
- package/dist/src/cli/commands/info.js +32 -0
- package/dist/src/cli/commands/info.js.map +1 -0
- package/dist/src/cli/commands/mcp.d.ts +6 -0
- package/dist/src/cli/commands/mcp.d.ts.map +1 -0
- package/dist/src/cli/commands/mcp.js +45 -0
- package/dist/src/cli/commands/mcp.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts +6 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -0
- package/dist/src/cli/commands/setup.js +59 -0
- package/dist/src/cli/commands/setup.js.map +1 -0
- package/dist/src/cli/commands/test/index.d.ts +6 -0
- package/dist/src/cli/commands/test/index.d.ts.map +1 -0
- package/dist/src/cli/commands/test/index.js +15 -0
- package/dist/src/cli/commands/test/index.js.map +1 -0
- package/dist/src/cli/commands/test/init.d.ts +6 -0
- package/dist/src/cli/commands/test/init.d.ts.map +1 -0
- package/dist/src/cli/commands/test/init.js +64 -0
- package/dist/src/cli/commands/test/init.js.map +1 -0
- package/dist/src/cli/commands/test/list.d.ts +6 -0
- package/dist/src/cli/commands/test/list.d.ts.map +1 -0
- package/dist/src/cli/commands/test/list.js +70 -0
- package/dist/src/cli/commands/test/list.js.map +1 -0
- package/dist/src/cli/commands/test/run.d.ts +6 -0
- package/dist/src/cli/commands/test/run.d.ts.map +1 -0
- package/dist/src/cli/commands/test/run.js +95 -0
- package/dist/src/cli/commands/test/run.js.map +1 -0
- package/dist/src/cli/commands/test/validate.d.ts +6 -0
- package/dist/src/cli/commands/test/validate.d.ts.map +1 -0
- package/dist/src/cli/commands/test/validate.js +70 -0
- package/dist/src/cli/commands/test/validate.js.map +1 -0
- package/dist/src/cli/index.d.ts +6 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +21 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/lib/config.d.ts +36 -0
- package/dist/src/cli/lib/config.d.ts.map +1 -0
- package/dist/src/cli/lib/config.js +89 -0
- package/dist/src/cli/lib/config.js.map +1 -0
- package/dist/src/cli/lib/loader.d.ts +49 -0
- package/dist/src/cli/lib/loader.d.ts.map +1 -0
- package/dist/src/cli/lib/loader.js +122 -0
- package/dist/src/cli/lib/loader.js.map +1 -0
- package/dist/src/cli/lib/output.d.ts +53 -0
- package/dist/src/cli/lib/output.d.ts.map +1 -0
- package/dist/src/cli/lib/output.js +133 -0
- package/dist/src/cli/lib/output.js.map +1 -0
- package/dist/src/cli/lib/runner.d.ts +23 -0
- package/dist/src/cli/lib/runner.d.ts.map +1 -0
- package/dist/src/cli/lib/runner.js +40 -0
- package/dist/src/cli/lib/runner.js.map +1 -0
- package/dist/src/http-server.d.ts +14 -0
- package/dist/src/http-server.d.ts.map +1 -0
- package/dist/src/http-server.js +145 -0
- package/dist/src/http-server.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server.d.ts +58 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +2376 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tunnel-mode.d.ts +13 -0
- package/dist/src/tunnel-mode.d.ts.map +1 -0
- package/dist/src/tunnel-mode.js +159 -0
- package/dist/src/tunnel-mode.js.map +1 -0
- package/dist/src/types/test-definition.d.ts +320 -0
- package/dist/src/types/test-definition.d.ts.map +1 -0
- package/dist/src/types/test-definition.js +11 -0
- package/dist/src/types/test-definition.js.map +1 -0
- package/dist/src/types.d.ts +209 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +34 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/package.d.ts +12 -0
- package/dist/src/utils/package.d.ts.map +1 -0
- package/dist/src/utils/package.js +36 -0
- package/dist/src/utils/package.js.map +1 -0
- package/dist/src/utils/summary.d.ts +45 -0
- package/dist/src/utils/summary.d.ts.map +1 -0
- package/dist/src/utils/summary.js +198 -0
- package/dist/src/utils/summary.js.map +1 -0
- package/lib/api/index.ts +977 -0
- package/lib/api/sse.ts +112 -0
- package/lib/browser/index.ts +181 -0
- package/lib/env/index.ts +156 -0
- package/lib/tunnel/index.test.ts +344 -0
- package/lib/tunnel/index.ts +197 -0
- package/lib/tunnel/integration.test.ts +98 -0
- package/package.json +100 -0
- 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
|