@debugg-ai/debugg-ai-mcp 1.0.62 → 1.0.64
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/dist/config/index.js +4 -1
- package/dist/handlers/createEnvironmentHandler.js +33 -0
- package/dist/handlers/createProjectHandler.js +62 -10
- package/dist/handlers/index.js +5 -14
- package/dist/handlers/searchEnvironmentsHandler.js +122 -0
- package/dist/handlers/searchExecutionsHandler.js +71 -0
- package/dist/handlers/searchProjectsHandler.js +72 -0
- package/dist/handlers/testPageChangesHandler.js +46 -5
- package/dist/handlers/triggerCrawlHandler.js +208 -0
- package/dist/handlers/updateEnvironmentHandler.js +94 -15
- package/dist/index.js +15 -2
- package/dist/services/workflows.js +10 -6
- package/dist/tools/createEnvironment.js +5 -1
- package/dist/tools/index.js +12 -42
- package/dist/tools/searchEnvironments.js +35 -0
- package/dist/tools/searchExecutions.js +31 -0
- package/dist/tools/searchProjects.js +30 -0
- package/dist/tools/triggerCrawl.js +99 -0
- package/dist/types/index.js +64 -71
- package/package.json +4 -1
- package/dist/handlers/cancelExecutionHandler.js +0 -41
- package/dist/handlers/createCredentialHandler.js +0 -60
- package/dist/handlers/deleteCredentialHandler.js +0 -51
- package/dist/handlers/getCredentialHandler.js +0 -49
- package/dist/handlers/getEnvironmentHandler.js +0 -49
- package/dist/handlers/getExecutionHandler.js +0 -37
- package/dist/handlers/getProjectHandler.js +0 -37
- package/dist/handlers/listCredentialsHandler.js +0 -93
- package/dist/handlers/listEnvironmentsHandler.js +0 -63
- package/dist/handlers/listExecutionsHandler.js +0 -35
- package/dist/handlers/listProjectsHandler.js +0 -32
- package/dist/handlers/listReposHandler.js +0 -27
- package/dist/handlers/listTeamsHandler.js +0 -27
- package/dist/handlers/updateCredentialHandler.js +0 -70
- package/dist/tools/cancelExecution.js +0 -22
- package/dist/tools/createCredential.js +0 -52
- package/dist/tools/deleteCredential.js +0 -24
- package/dist/tools/getCredential.js +0 -24
- package/dist/tools/getEnvironment.js +0 -23
- package/dist/tools/getExecution.js +0 -22
- package/dist/tools/getProject.js +0 -22
- package/dist/tools/listCredentials.js +0 -30
- package/dist/tools/listEnvironments.js +0 -28
- package/dist/tools/listExecutions.js +0 -24
- package/dist/tools/listProjects.js +0 -27
- package/dist/tools/listRepos.js +0 -23
- package/dist/tools/listTeams.js +0 -23
- package/dist/tools/updateCredential.js +0 -28
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger Crawl Handler
|
|
3
|
+
*
|
|
4
|
+
* Executes the Raw Crawl Workflow Template via the 4-step pattern shared with
|
|
5
|
+
* testPageChangesHandler:
|
|
6
|
+
* find template → provision tunnel if localhost → execute → poll → result
|
|
7
|
+
*
|
|
8
|
+
* Unlike check_app_in_browser, a crawl does NOT return pass/fail — it returns
|
|
9
|
+
* the execution status + metadata. The backend's job is to explore the app
|
|
10
|
+
* and populate the project knowledge graph; this handler just triggers it
|
|
11
|
+
* and reports back what happened.
|
|
12
|
+
*/
|
|
13
|
+
import { config } from '../config/index.js';
|
|
14
|
+
import { Logger } from '../utils/logger.js';
|
|
15
|
+
import { handleExternalServiceError } from '../utils/errors.js';
|
|
16
|
+
import { DebuggAIServerClient } from '../services/index.js';
|
|
17
|
+
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
18
|
+
const logger = new Logger({ module: 'triggerCrawlHandler' });
|
|
19
|
+
const TEMPLATE_KEYWORD = 'raw crawl';
|
|
20
|
+
export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
logger.toolStart('trigger_crawl', input);
|
|
23
|
+
// Bead 0bq: progress circuit-breaker — see testPageChangesHandler for rationale.
|
|
24
|
+
let progressDisabled = false;
|
|
25
|
+
const progressCallback = rawProgressCallback
|
|
26
|
+
? async (update) => {
|
|
27
|
+
if (progressDisabled)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
await rawProgressCallback(update);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
progressDisabled = true;
|
|
34
|
+
logger.warn('Progress emission failed; disabling further emissions for this request', {
|
|
35
|
+
error: err instanceof Error ? err.message : String(err),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
41
|
+
await client.init();
|
|
42
|
+
const originalUrl = resolveTargetUrl(input);
|
|
43
|
+
let ctx = buildContext(originalUrl);
|
|
44
|
+
let keyId;
|
|
45
|
+
const abortController = new AbortController();
|
|
46
|
+
const onStdinClose = () => {
|
|
47
|
+
abortController.abort();
|
|
48
|
+
progressDisabled = true;
|
|
49
|
+
};
|
|
50
|
+
process.stdin.once('close', onStdinClose);
|
|
51
|
+
try {
|
|
52
|
+
// --- Tunnel: reuse existing or provision a fresh one ---
|
|
53
|
+
if (ctx.isLocalhost) {
|
|
54
|
+
if (progressCallback) {
|
|
55
|
+
await progressCallback({ progress: 1, total: 4, message: 'Provisioning secure tunnel for localhost...' });
|
|
56
|
+
}
|
|
57
|
+
const reused = findExistingTunnel(ctx);
|
|
58
|
+
if (reused) {
|
|
59
|
+
ctx = reused;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
let tunnel;
|
|
63
|
+
try {
|
|
64
|
+
tunnel = await client.tunnels.provision();
|
|
65
|
+
}
|
|
66
|
+
catch (provisionError) {
|
|
67
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
68
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
69
|
+
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
70
|
+
`(Detail: ${msg})`);
|
|
71
|
+
}
|
|
72
|
+
keyId = tunnel.keyId;
|
|
73
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// --- Find the crawl workflow template ---
|
|
77
|
+
if (progressCallback) {
|
|
78
|
+
await progressCallback({ progress: 2, total: 4, message: 'Locating crawl workflow template...' });
|
|
79
|
+
}
|
|
80
|
+
const template = await client.workflows.findTemplateByName(TEMPLATE_KEYWORD);
|
|
81
|
+
if (!template) {
|
|
82
|
+
throw new Error(`Raw Crawl Workflow Template not found. ` +
|
|
83
|
+
`Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
|
|
84
|
+
}
|
|
85
|
+
const templateUuid = template.uuid;
|
|
86
|
+
logger.info(`Using crawl template: ${template.name} (${templateUuid})`);
|
|
87
|
+
// --- Build contextData + env ---
|
|
88
|
+
const contextData = {
|
|
89
|
+
targetUrl: ctx.targetUrl ?? ctx.originalUrl,
|
|
90
|
+
};
|
|
91
|
+
if (input.projectUuid)
|
|
92
|
+
contextData.projectId = input.projectUuid;
|
|
93
|
+
if (typeof input.headless === 'boolean')
|
|
94
|
+
contextData.headless = input.headless;
|
|
95
|
+
if (typeof input.timeoutSeconds === 'number')
|
|
96
|
+
contextData.timeoutSeconds = input.timeoutSeconds;
|
|
97
|
+
const env = {};
|
|
98
|
+
if (input.environmentId)
|
|
99
|
+
env.environmentId = input.environmentId;
|
|
100
|
+
if (input.credentialId)
|
|
101
|
+
env.credentialId = input.credentialId;
|
|
102
|
+
if (input.credentialRole)
|
|
103
|
+
env.credentialRole = input.credentialRole;
|
|
104
|
+
if (input.username)
|
|
105
|
+
env.username = input.username;
|
|
106
|
+
if (input.password)
|
|
107
|
+
env.password = input.password;
|
|
108
|
+
// --- Execute ---
|
|
109
|
+
if (progressCallback) {
|
|
110
|
+
await progressCallback({ progress: 3, total: 4, message: 'Queuing crawl execution...' });
|
|
111
|
+
}
|
|
112
|
+
const executeResponse = await client.workflows.executeWorkflow(templateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
|
|
113
|
+
const executionUuid = executeResponse.executionUuid;
|
|
114
|
+
logger.info(`Crawl execution queued: ${executionUuid}`);
|
|
115
|
+
// --- Poll ---
|
|
116
|
+
// Bead 0bq: emit the final progress (4/4 "Complete:...") INSIDE onUpdate
|
|
117
|
+
// when terminal status detected, so there's no post-resolve emission that
|
|
118
|
+
// could race the response and cause stale-progressToken transport tear-down.
|
|
119
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
120
|
+
const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
|
|
121
|
+
if (ctx.tunnelId)
|
|
122
|
+
touchTunnelById(ctx.tunnelId);
|
|
123
|
+
if (!progressCallback)
|
|
124
|
+
return;
|
|
125
|
+
const nodeCount = (exec.nodeExecutions ?? []).length;
|
|
126
|
+
if (TERMINAL_STATUSES.has(exec.status)) {
|
|
127
|
+
await progressCallback({
|
|
128
|
+
progress: 4, total: 4,
|
|
129
|
+
message: `Crawl ${exec.status} (${nodeCount} nodes)`,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await progressCallback({
|
|
134
|
+
progress: 4, total: 4,
|
|
135
|
+
message: `Crawl ${exec.status} (${nodeCount} nodes)`,
|
|
136
|
+
});
|
|
137
|
+
}, abortController.signal);
|
|
138
|
+
const duration = Date.now() - startTime;
|
|
139
|
+
const nodes = finalExecution.nodeExecutions ?? [];
|
|
140
|
+
// --- Format response ---
|
|
141
|
+
const responsePayload = {
|
|
142
|
+
executionId: executionUuid,
|
|
143
|
+
status: finalExecution.status,
|
|
144
|
+
targetUrl: ctx.originalUrl,
|
|
145
|
+
durationMs: finalExecution.durationMs ?? duration,
|
|
146
|
+
};
|
|
147
|
+
const outcome = finalExecution.state?.outcome;
|
|
148
|
+
if (outcome !== undefined && outcome !== null)
|
|
149
|
+
responsePayload.outcome = outcome;
|
|
150
|
+
if (finalExecution.errorMessage)
|
|
151
|
+
responsePayload.errorMessage = finalExecution.errorMessage;
|
|
152
|
+
if (finalExecution.errorInfo?.failedNodeId)
|
|
153
|
+
responsePayload.failedNode = finalExecution.errorInfo.failedNodeId;
|
|
154
|
+
if (executeResponse.resolvedEnvironmentId)
|
|
155
|
+
responsePayload.resolvedEnvironmentId = executeResponse.resolvedEnvironmentId;
|
|
156
|
+
if (executeResponse.resolvedCredentialId)
|
|
157
|
+
responsePayload.resolvedCredentialId = executeResponse.resolvedCredentialId;
|
|
158
|
+
// Extract crawl metrics from surfer.crawl node (absent in older graph shapes)
|
|
159
|
+
const crawlNode = nodes.find(n => n.nodeType === 'surfer.crawl');
|
|
160
|
+
if (crawlNode?.outputData) {
|
|
161
|
+
const d = crawlNode.outputData;
|
|
162
|
+
responsePayload.crawlSummary = {
|
|
163
|
+
pagesDiscovered: d.pagesDiscovered,
|
|
164
|
+
actionsExecuted: d.actionsExecuted,
|
|
165
|
+
stepsTaken: d.stepsTaken,
|
|
166
|
+
transitionsRecorded: d.transitionsRecorded,
|
|
167
|
+
knowledgeGraphStates: d.knowledgeGraphStates,
|
|
168
|
+
success: d.success,
|
|
169
|
+
...(d.error ? { error: d.error } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Extract KG import result from knowledge_graph.import node (absent in older graph shapes)
|
|
173
|
+
const kgNode = nodes.find(n => n.nodeType === 'knowledge_graph.import');
|
|
174
|
+
if (kgNode?.outputData) {
|
|
175
|
+
const d = kgNode.outputData;
|
|
176
|
+
responsePayload.knowledgeGraph = {
|
|
177
|
+
imported: !d.skipped,
|
|
178
|
+
skipped: d.skipped ?? false,
|
|
179
|
+
reason: d.reason ?? '',
|
|
180
|
+
edgesImported: d.edgesImported ?? 0,
|
|
181
|
+
statesImported: d.statesImported ?? 0,
|
|
182
|
+
knowledgeGraphId: d.knowledgeGraphId ?? '',
|
|
183
|
+
...(Array.isArray(d.importErrors) && d.importErrors.length > 0 ? { importErrors: d.importErrors } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
logger.toolComplete('trigger_crawl', duration);
|
|
187
|
+
// Bead 0bq: final progress is emitted INSIDE pollExecution's onUpdate when
|
|
188
|
+
// terminal status is detected. Emitting it here would race the response
|
|
189
|
+
// and could cause stale-progressToken transport tear-down.
|
|
190
|
+
const sanitizedPayload = sanitizeResponseUrls(responsePayload, ctx);
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const duration = Date.now() - startTime;
|
|
197
|
+
logger.toolError('trigger_crawl', error, duration);
|
|
198
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'crawl execution');
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
process.stdin.removeListener('close', onStdinClose);
|
|
202
|
+
// Tunnel intentionally NOT torn down (reuse path per bead vwd).
|
|
203
|
+
// If tunnel creation failed after key provision, revoke the orphaned key.
|
|
204
|
+
if (!ctx.tunnelId && keyId) {
|
|
205
|
+
client.revokeNgrokKey(keyId).catch(err => logger.warn(`Failed to revoke unused ngrok key ${keyId}: ${err}`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -14,11 +14,23 @@ function notFound(uuid, context) {
|
|
|
14
14
|
isError: true,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
function stripPassword(c) {
|
|
18
|
+
return {
|
|
19
|
+
uuid: c.uuid,
|
|
20
|
+
label: c.label,
|
|
21
|
+
username: c.username,
|
|
22
|
+
role: c.role ?? null,
|
|
23
|
+
environmentUuid: c.environmentUuid,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
17
26
|
export async function updateEnvironmentHandler(input, _context) {
|
|
18
27
|
const start = Date.now();
|
|
19
28
|
logger.toolStart('update_environment', {
|
|
20
29
|
uuid: input.uuid,
|
|
21
|
-
|
|
30
|
+
hasEnvPatch: !!(input.name || input.url || input.description),
|
|
31
|
+
addCount: input.addCredentials?.length ?? 0,
|
|
32
|
+
updateCount: input.updateCredentials?.length ?? 0,
|
|
33
|
+
removeCount: input.removeCredentialIds?.length ?? 0,
|
|
22
34
|
projectUuid: input.projectUuid,
|
|
23
35
|
});
|
|
24
36
|
try {
|
|
@@ -34,23 +46,90 @@ export async function updateEnvironmentHandler(input, _context) {
|
|
|
34
46
|
return notFound(input.uuid, `no project found for repo "${repoName}"`);
|
|
35
47
|
projectUuid = project.uuid;
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
// ── Env field patch (only if any env field is present) ──────────────────
|
|
50
|
+
const hasEnvPatch = input.name !== undefined || input.url !== undefined || input.description !== undefined;
|
|
51
|
+
let environment = null;
|
|
52
|
+
if (hasEnvPatch) {
|
|
53
|
+
try {
|
|
54
|
+
environment = await client.updateEnvironment(projectUuid, input.uuid, {
|
|
55
|
+
name: input.name, url: input.url, description: input.description,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err?.statusCode === 404 || err?.response?.status === 404) {
|
|
60
|
+
return notFound(input.uuid, `backend returned 404 for project ${projectUuid}`);
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Echo the uuid so the response shape stays consistent. Include projectUuid
|
|
67
|
+
// for downstream tooling. Only populate `environment` if we patched.
|
|
68
|
+
environment = { uuid: input.uuid };
|
|
69
|
+
}
|
|
70
|
+
// ── Cred sub-actions (remove → update → add) ────────────────────────────
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const addedCredentials = [];
|
|
73
|
+
const updatedCredentials = [];
|
|
74
|
+
const removedCredentialIds = [];
|
|
75
|
+
if (input.removeCredentialIds) {
|
|
76
|
+
for (const credUuid of input.removeCredentialIds) {
|
|
77
|
+
try {
|
|
78
|
+
await client.deleteCredential(projectUuid, input.uuid, credUuid);
|
|
79
|
+
removedCredentialIds.push(credUuid);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
warnings.push({ op: 'remove', uuid: credUuid, error: err?.message ?? String(err) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (input.updateCredentials) {
|
|
87
|
+
for (const patch of input.updateCredentials) {
|
|
88
|
+
try {
|
|
89
|
+
const updated = await client.updateCredential(projectUuid, input.uuid, patch.uuid, {
|
|
90
|
+
label: patch.label,
|
|
91
|
+
username: patch.username,
|
|
92
|
+
password: patch.password,
|
|
93
|
+
role: patch.role,
|
|
94
|
+
});
|
|
95
|
+
updatedCredentials.push(stripPassword(updated));
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
warnings.push({ op: 'update', uuid: patch.uuid, error: err?.message ?? String(err) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
47
101
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
if (input.addCredentials) {
|
|
103
|
+
for (const seed of input.addCredentials) {
|
|
104
|
+
try {
|
|
105
|
+
const cred = await client.createCredential(projectUuid, input.uuid, {
|
|
106
|
+
label: seed.label,
|
|
107
|
+
username: seed.username,
|
|
108
|
+
password: seed.password,
|
|
109
|
+
role: seed.role,
|
|
110
|
+
});
|
|
111
|
+
addedCredentials.push(stripPassword(cred));
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
warnings.push({ op: 'add', label: seed.label, error: err?.message ?? String(err) });
|
|
115
|
+
}
|
|
51
116
|
}
|
|
52
|
-
throw err;
|
|
53
117
|
}
|
|
118
|
+
// ── Build response ──────────────────────────────────────────────────────
|
|
119
|
+
const payload = {
|
|
120
|
+
updated: hasEnvPatch,
|
|
121
|
+
environment,
|
|
122
|
+
};
|
|
123
|
+
if (addedCredentials.length > 0)
|
|
124
|
+
payload.addedCredentials = addedCredentials;
|
|
125
|
+
if (updatedCredentials.length > 0)
|
|
126
|
+
payload.updatedCredentials = updatedCredentials;
|
|
127
|
+
if (removedCredentialIds.length > 0)
|
|
128
|
+
payload.removedCredentialIds = removedCredentialIds;
|
|
129
|
+
if (warnings.length > 0)
|
|
130
|
+
payload.credentialWarnings = warnings;
|
|
131
|
+
logger.toolComplete('update_environment', Date.now() - start);
|
|
132
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
54
133
|
}
|
|
55
134
|
catch (error) {
|
|
56
135
|
logger.toolError('update_environment', error, Date.now() - start);
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
22
22
|
import { config } from "./config/index.js";
|
|
23
23
|
import { initTools, getTools, getTool } from "./tools/index.js";
|
|
24
24
|
import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, } from "./utils/index.js";
|
|
25
|
+
import { MCPErrorCode, MCPError, } from "./types/index.js";
|
|
25
26
|
// Logger and server are initialized lazily in main() to avoid triggering
|
|
26
27
|
// config loading at module load time. If config validation fails (bad env vars),
|
|
27
28
|
// the error is caught by main()'s try-catch instead of crashing before any
|
|
@@ -91,6 +92,16 @@ function registerHandlers() {
|
|
|
91
92
|
requestLogger.warn(`Tool not found: ${name}`);
|
|
92
93
|
throw new Error(`Unknown tool: ${name}`);
|
|
93
94
|
}
|
|
95
|
+
// Deferred config validation (bead cma): if DEBUGGAI_API_KEY is missing,
|
|
96
|
+
// return a structured error MCP clients can surface in their UI — instead
|
|
97
|
+
// of letting the backend return a cryptic 401.
|
|
98
|
+
if (!config.api.key) {
|
|
99
|
+
const mcpError = new MCPError(MCPErrorCode.CONFIGURATION_ERROR, 'DEBUGGAI_API_KEY is not set. ' +
|
|
100
|
+
'Configure it in your MCP server registration (e.g. `claude mcp add debugg-ai -s user -e DEBUGGAI_API_KEY=<your-key> -- npx -y @debugg-ai/debugg-ai-mcp`). ' +
|
|
101
|
+
'Get a key at https://debugg.ai.', { missingEnvVars: ['DEBUGGAI_API_KEY'] });
|
|
102
|
+
requestLogger.warn('Tool call blocked — DEBUGGAI_API_KEY missing', { tool: name });
|
|
103
|
+
return createErrorResponse(mcpError, name);
|
|
104
|
+
}
|
|
94
105
|
try {
|
|
95
106
|
const validatedInput = validateInput(tool.inputSchema, args, name);
|
|
96
107
|
const context = {
|
|
@@ -141,9 +152,11 @@ async function main() {
|
|
|
141
152
|
architecture: process.arch,
|
|
142
153
|
pid: process.pid
|
|
143
154
|
});
|
|
144
|
-
//
|
|
155
|
+
// NOTE: DEBUGGAI_API_KEY validation is deferred to the first tool call so
|
|
156
|
+
// MCP clients see a proper initialize response + a structured tool error,
|
|
157
|
+
// rather than the subprocess dying with "Failed to reconnect" (bead cma).
|
|
145
158
|
if (!config.api.key) {
|
|
146
|
-
|
|
159
|
+
logger.warn('DEBUGGAI_API_KEY is not set. Server will boot but every tool call will return a ConfigurationError until the env var is configured.');
|
|
147
160
|
}
|
|
148
161
|
// Initialize telemetry (PostHog when key is set, Noop otherwise)
|
|
149
162
|
Telemetry.setDistinctId(config.api.key);
|
|
@@ -8,18 +8,22 @@ const POLL_INTERVAL_MS = 3000;
|
|
|
8
8
|
const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
|
|
9
9
|
export const createWorkflowsService = (tx) => {
|
|
10
10
|
const service = {
|
|
11
|
-
async
|
|
11
|
+
async findTemplateByName(keyword) {
|
|
12
12
|
const response = await tx.get('api/v1/workflows/', { isTemplate: true });
|
|
13
13
|
const templates = response?.results ?? [];
|
|
14
14
|
if (templates.length === 0)
|
|
15
15
|
return null;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const needle = keyword.toLowerCase();
|
|
17
|
+
const match = templates.find(t => t.name.toLowerCase().includes(needle));
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error(`No workflow template matching "${keyword}" found. ` +
|
|
19
20
|
`Available templates: ${templates.map(t => `"${t.name}"`).join(', ')}. ` +
|
|
20
|
-
`Ensure the backend has a template with "
|
|
21
|
+
`Ensure the backend has a template with "${keyword}" in its name.`);
|
|
21
22
|
}
|
|
22
|
-
return
|
|
23
|
+
return match;
|
|
24
|
+
},
|
|
25
|
+
async findEvaluationTemplate() {
|
|
26
|
+
return service.findTemplateByName('app evaluation');
|
|
23
27
|
},
|
|
24
28
|
async executeWorkflow(workflowUuid, contextData, env) {
|
|
25
29
|
const body = { contextData };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { CreateEnvironmentInputSchema } from '../types/index.js';
|
|
2
2
|
import { createEnvironmentHandler } from '../handlers/createEnvironmentHandler.js';
|
|
3
|
-
const DESCRIPTION = `Create a new environment under a DebuggAI project. Both name and url are required (backend rejects standard environments without a URL). Optional description. Defaults to the project resolved from the current git repo; pass projectUuid to target a different project (get UUIDs via
|
|
3
|
+
const DESCRIPTION = `Create a new environment under a DebuggAI project. Both name and url are required (backend rejects standard environments without a URL). Optional description. Defaults to the project resolved from the current git repo; pass projectUuid to target a different project (get UUIDs via search_projects).
|
|
4
|
+
|
|
5
|
+
OPTIONAL credentials seed: pass \`credentials: [{label, username, password, role?}]\` to create login credentials alongside the environment in a single call. Each cred is created best-effort; failures go to \`credentialWarnings\` without blocking env creation. Passwords are write-only and NEVER returned.
|
|
6
|
+
|
|
7
|
+
Returns the created environment's uuid (and the seeded credentials, if any). Reference the env uuid when running check_app_in_browser.`;
|
|
4
8
|
export function buildCreateEnvironmentTool() {
|
|
5
9
|
return {
|
|
6
10
|
name: 'create_environment',
|
package/dist/tools/index.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { buildTestPageChangesTool, buildValidatedTestPageChangesTool } from './testPageChanges.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { buildTriggerCrawlTool, buildValidatedTriggerCrawlTool } from './triggerCrawl.js';
|
|
3
|
+
import { buildSearchProjectsTool, buildValidatedSearchProjectsTool } from './searchProjects.js';
|
|
4
|
+
import { buildSearchEnvironmentsTool, buildValidatedSearchEnvironmentsTool } from './searchEnvironments.js';
|
|
5
|
+
import { buildSearchExecutionsTool, buildValidatedSearchExecutionsTool } from './searchExecutions.js';
|
|
5
6
|
import { buildCreateEnvironmentTool, buildValidatedCreateEnvironmentTool } from './createEnvironment.js';
|
|
6
|
-
import { buildCreateCredentialTool, buildValidatedCreateCredentialTool } from './createCredential.js';
|
|
7
|
-
import { buildGetEnvironmentTool, buildValidatedGetEnvironmentTool } from './getEnvironment.js';
|
|
8
7
|
import { buildUpdateEnvironmentTool, buildValidatedUpdateEnvironmentTool } from './updateEnvironment.js';
|
|
9
8
|
import { buildDeleteEnvironmentTool, buildValidatedDeleteEnvironmentTool } from './deleteEnvironment.js';
|
|
10
|
-
import { buildGetCredentialTool, buildValidatedGetCredentialTool } from './getCredential.js';
|
|
11
|
-
import { buildUpdateCredentialTool, buildValidatedUpdateCredentialTool } from './updateCredential.js';
|
|
12
|
-
import { buildDeleteCredentialTool, buildValidatedDeleteCredentialTool } from './deleteCredential.js';
|
|
13
|
-
import { buildGetProjectTool, buildValidatedGetProjectTool } from './getProject.js';
|
|
14
9
|
import { buildUpdateProjectTool, buildValidatedUpdateProjectTool } from './updateProject.js';
|
|
15
10
|
import { buildDeleteProjectTool, buildValidatedDeleteProjectTool } from './deleteProject.js';
|
|
16
|
-
import { buildListExecutionsTool, buildValidatedListExecutionsTool } from './listExecutions.js';
|
|
17
|
-
import { buildGetExecutionTool, buildValidatedGetExecutionTool } from './getExecution.js';
|
|
18
|
-
import { buildCancelExecutionTool, buildValidatedCancelExecutionTool } from './cancelExecution.js';
|
|
19
|
-
import { buildListTeamsTool, buildValidatedListTeamsTool } from './listTeams.js';
|
|
20
|
-
import { buildListReposTool, buildValidatedListReposTool } from './listRepos.js';
|
|
21
11
|
import { buildCreateProjectTool, buildValidatedCreateProjectTool } from './createProject.js';
|
|
22
12
|
let _tools = null;
|
|
23
13
|
let _validatedTools = null;
|
|
@@ -28,48 +18,28 @@ const toolRegistry = new Map();
|
|
|
28
18
|
export function initTools(ctx) {
|
|
29
19
|
const tools = [
|
|
30
20
|
buildTestPageChangesTool(ctx),
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
buildTriggerCrawlTool(ctx),
|
|
22
|
+
buildSearchProjectsTool(),
|
|
23
|
+
buildSearchEnvironmentsTool(),
|
|
34
24
|
buildCreateEnvironmentTool(),
|
|
35
|
-
buildCreateCredentialTool(),
|
|
36
|
-
buildGetEnvironmentTool(),
|
|
37
25
|
buildUpdateEnvironmentTool(),
|
|
38
26
|
buildDeleteEnvironmentTool(),
|
|
39
|
-
buildGetCredentialTool(),
|
|
40
|
-
buildUpdateCredentialTool(),
|
|
41
|
-
buildDeleteCredentialTool(),
|
|
42
|
-
buildGetProjectTool(),
|
|
43
27
|
buildUpdateProjectTool(),
|
|
44
28
|
buildDeleteProjectTool(),
|
|
45
|
-
|
|
46
|
-
buildGetExecutionTool(),
|
|
47
|
-
buildCancelExecutionTool(),
|
|
48
|
-
buildListTeamsTool(),
|
|
49
|
-
buildListReposTool(),
|
|
29
|
+
buildSearchExecutionsTool(),
|
|
50
30
|
buildCreateProjectTool(),
|
|
51
31
|
];
|
|
52
32
|
const validated = [
|
|
53
33
|
buildValidatedTestPageChangesTool(ctx),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
buildValidatedTriggerCrawlTool(ctx),
|
|
35
|
+
buildValidatedSearchProjectsTool(),
|
|
36
|
+
buildValidatedSearchEnvironmentsTool(),
|
|
57
37
|
buildValidatedCreateEnvironmentTool(),
|
|
58
|
-
buildValidatedCreateCredentialTool(),
|
|
59
|
-
buildValidatedGetEnvironmentTool(),
|
|
60
38
|
buildValidatedUpdateEnvironmentTool(),
|
|
61
39
|
buildValidatedDeleteEnvironmentTool(),
|
|
62
|
-
buildValidatedGetCredentialTool(),
|
|
63
|
-
buildValidatedUpdateCredentialTool(),
|
|
64
|
-
buildValidatedDeleteCredentialTool(),
|
|
65
|
-
buildValidatedGetProjectTool(),
|
|
66
40
|
buildValidatedUpdateProjectTool(),
|
|
67
41
|
buildValidatedDeleteProjectTool(),
|
|
68
|
-
|
|
69
|
-
buildValidatedGetExecutionTool(),
|
|
70
|
-
buildValidatedCancelExecutionTool(),
|
|
71
|
-
buildValidatedListTeamsTool(),
|
|
72
|
-
buildValidatedListReposTool(),
|
|
42
|
+
buildValidatedSearchExecutionsTool(),
|
|
73
43
|
buildValidatedCreateProjectTool(),
|
|
74
44
|
];
|
|
75
45
|
_tools = tools;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SearchEnvironmentsInputSchema } from '../types/index.js';
|
|
2
|
+
import { searchEnvironmentsHandler } from '../handlers/searchEnvironmentsHandler.js';
|
|
3
|
+
const DESCRIPTION = `Search or look up environments, with credentials expanded inline per environment.
|
|
4
|
+
|
|
5
|
+
Two modes:
|
|
6
|
+
- uuid mode: {"uuid": "<env-uuid>"} → single env with full detail + its credentials. NotFound if the uuid doesn't exist.
|
|
7
|
+
- filter mode: omit uuid, optionally {"q": "<keyword>", "projectUuid", "page", "pageSize"} → paginated envs, each with its credentials.
|
|
8
|
+
|
|
9
|
+
Project resolution: if projectUuid is omitted, the current git repo's origin is auto-resolved to a DebuggAI project. Returns {error:"NoProjectResolved", environments:[]} if neither is available.
|
|
10
|
+
|
|
11
|
+
Credentials are returned inline per env as {uuid, label, username, role}. Password is NEVER returned — the handler defensively strips it regardless of what the service layer provides.
|
|
12
|
+
|
|
13
|
+
Response: {project, filter, pageInfo, environments[]} — each environment includes a credentials[] array.`;
|
|
14
|
+
export function buildSearchEnvironmentsTool() {
|
|
15
|
+
return {
|
|
16
|
+
name: 'search_environments',
|
|
17
|
+
title: 'Search Environments',
|
|
18
|
+
description: DESCRIPTION,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
uuid: { type: 'string', description: 'Environment UUID. Returns single env with credentials inline. Mutually exclusive with projectUuid/q filter params.' },
|
|
23
|
+
projectUuid: { type: 'string', description: 'Override the auto-detected project. Used in filter mode.' },
|
|
24
|
+
q: { type: 'string', description: 'Free-text search over environment name. Mutually exclusive with uuid.' },
|
|
25
|
+
page: { type: 'number', description: 'Page number (1-indexed).' },
|
|
26
|
+
pageSize: { type: 'number', description: 'Page size (1..200). Default 20.' },
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function buildValidatedSearchEnvironmentsTool() {
|
|
33
|
+
const tool = buildSearchEnvironmentsTool();
|
|
34
|
+
return { ...tool, inputSchema: SearchEnvironmentsInputSchema, handler: searchEnvironmentsHandler };
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SearchExecutionsInputSchema } from '../types/index.js';
|
|
2
|
+
import { searchExecutionsHandler } from '../handlers/searchExecutionsHandler.js';
|
|
3
|
+
const DESCRIPTION = `Search or look up workflow executions (history of check_app_in_browser, trigger_crawl, and other workflow runs).
|
|
4
|
+
|
|
5
|
+
Two modes:
|
|
6
|
+
- uuid mode: {"uuid": "<execution-uuid>"} → single execution with FULL detail including nodeExecutions, state, errorInfo. NotFound if the uuid doesn't exist.
|
|
7
|
+
- filter mode: {"status": "completed"|"running"|"failed"|"cancelled", "projectUuid": "...", "page", "pageSize"} → paginated summaries.
|
|
8
|
+
|
|
9
|
+
Response shape: {filter, pageInfo, executions[]}. Summary items have outcome/status/durationMs/timestamps; uuid-mode items additionally have nodeExecutions + state + errorInfo.`;
|
|
10
|
+
export function buildSearchExecutionsTool() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'search_executions',
|
|
13
|
+
title: 'Search Workflow Executions',
|
|
14
|
+
description: DESCRIPTION,
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
uuid: { type: 'string', description: 'Execution UUID. Returns single execution with full detail. Mutually exclusive with projectUuid/status filters.' },
|
|
19
|
+
projectUuid: { type: 'string', description: 'Filter by project UUID.' },
|
|
20
|
+
status: { type: 'string', description: 'Filter by status: completed | running | failed | cancelled | pending.' },
|
|
21
|
+
page: { type: 'number', description: 'Page number (1-indexed).' },
|
|
22
|
+
pageSize: { type: 'number', description: 'Page size (1..200). Default 20.' },
|
|
23
|
+
},
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function buildValidatedSearchExecutionsTool() {
|
|
29
|
+
const tool = buildSearchExecutionsTool();
|
|
30
|
+
return { ...tool, inputSchema: SearchExecutionsInputSchema, handler: searchExecutionsHandler };
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SearchProjectsInputSchema } from '../types/index.js';
|
|
2
|
+
import { searchProjectsHandler } from '../handlers/searchProjectsHandler.js';
|
|
3
|
+
const DESCRIPTION = `Search or look up projects.
|
|
4
|
+
|
|
5
|
+
Two modes:
|
|
6
|
+
- uuid mode: pass {"uuid": "<project-uuid>"} → returns that project with the curated detail view (uuid, name, slug, platform, repoName, description, status, language, framework, timestamp, lastMod), or isError:true NotFound.
|
|
7
|
+
- filter mode: omit uuid, optionally pass {"q": "<keyword>", "page": 1, "pageSize": 20} → returns a paginated list of summaries (uuid, name, slug, repoName).
|
|
8
|
+
|
|
9
|
+
Response shape is always {filter, pageInfo, projects[]}. uuid mode returns exactly one project; filter mode returns summaries.`;
|
|
10
|
+
export function buildSearchProjectsTool() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'search_projects',
|
|
13
|
+
title: 'Search Projects',
|
|
14
|
+
description: DESCRIPTION,
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
uuid: { type: 'string', description: 'Project UUID. When provided, returns exactly that project with full detail. Mutually exclusive with q.' },
|
|
19
|
+
q: { type: 'string', description: 'Free-text search (backend-side). Mutually exclusive with uuid.' },
|
|
20
|
+
page: { type: 'number', description: 'Page number (1-indexed). Default 1.' },
|
|
21
|
+
pageSize: { type: 'number', description: 'Page size (1..200). Default 20.' },
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function buildValidatedSearchProjectsTool() {
|
|
28
|
+
const tool = buildSearchProjectsTool();
|
|
29
|
+
return { ...tool, inputSchema: SearchProjectsInputSchema, handler: searchProjectsHandler };
|
|
30
|
+
}
|