@debugg-ai/debugg-ai-mcp 1.0.63 → 1.0.65
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/CHANGELOG.md +177 -0
- package/README.md +49 -37
- package/dist/config/index.js +4 -1
- package/dist/handlers/createEnvironmentHandler.js +34 -1
- package/dist/handlers/createProjectHandler.js +62 -10
- package/dist/handlers/index.js +4 -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 +37 -7
- package/dist/handlers/updateEnvironmentHandler.js +94 -15
- package/dist/index.js +15 -2
- package/dist/services/index.js +3 -3
- package/dist/tools/createEnvironment.js +5 -1
- package/dist/tools/createProject.js +6 -4
- package/dist/tools/index.js +9 -42
- package/dist/tools/searchEnvironments.js +35 -0
- package/dist/tools/searchExecutions.js +31 -0
- package/dist/tools/searchProjects.js +30 -0
- package/dist/types/index.js +52 -71
- package/package.json +8 -2
- 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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_executions handler (bead 49b)
|
|
3
|
+
*
|
|
4
|
+
* Absorbs list_executions + get_execution.
|
|
5
|
+
*
|
|
6
|
+
* Modes:
|
|
7
|
+
* uuid: {uuid} → {filter:{uuid}, pageInfo:{totalCount:1,...}, executions:[fullDetail]}
|
|
8
|
+
* fullDetail includes nodeExecutions, state, errorInfo.
|
|
9
|
+
* filter: {status?, projectUuid?, page?, pageSize?} → {filter, pageInfo, executions:[summary]}
|
|
10
|
+
*/
|
|
11
|
+
import { Logger } from '../utils/logger.js';
|
|
12
|
+
import { handleExternalServiceError } from '../utils/errors.js';
|
|
13
|
+
import { DebuggAIServerClient } from '../services/index.js';
|
|
14
|
+
import { config } from '../config/index.js';
|
|
15
|
+
import { toPaginationParams } from '../utils/pagination.js';
|
|
16
|
+
const logger = new Logger({ module: 'searchExecutionsHandler' });
|
|
17
|
+
function notFound(uuid) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{
|
|
20
|
+
type: 'text',
|
|
21
|
+
text: JSON.stringify({ error: 'NotFound', message: `Execution ${uuid} not found.`, uuid }, null, 2),
|
|
22
|
+
}],
|
|
23
|
+
isError: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function searchExecutionsHandler(input, _context) {
|
|
27
|
+
const start = Date.now();
|
|
28
|
+
logger.toolStart('search_executions', input);
|
|
29
|
+
try {
|
|
30
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
31
|
+
await client.init();
|
|
32
|
+
if (input.uuid) {
|
|
33
|
+
try {
|
|
34
|
+
const execution = await client.workflows.getExecution(input.uuid);
|
|
35
|
+
const payload = {
|
|
36
|
+
filter: { uuid: input.uuid },
|
|
37
|
+
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
|
|
38
|
+
executions: [execution],
|
|
39
|
+
};
|
|
40
|
+
logger.toolComplete('search_executions', Date.now() - start);
|
|
41
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err?.statusCode === 404 || err?.response?.status === 404)
|
|
45
|
+
return notFound(input.uuid);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
|
|
50
|
+
const { pageInfo, executions } = await client.workflows.listExecutions({
|
|
51
|
+
status: input.status,
|
|
52
|
+
projectId: input.projectUuid,
|
|
53
|
+
page: pagination.page,
|
|
54
|
+
pageSize: pagination.pageSize,
|
|
55
|
+
});
|
|
56
|
+
const payload = {
|
|
57
|
+
filter: {
|
|
58
|
+
status: input.status ?? null,
|
|
59
|
+
projectUuid: input.projectUuid ?? null,
|
|
60
|
+
},
|
|
61
|
+
pageInfo,
|
|
62
|
+
executions,
|
|
63
|
+
};
|
|
64
|
+
logger.toolComplete('search_executions', Date.now() - start);
|
|
65
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
logger.toolError('search_executions', error, Date.now() - start);
|
|
69
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'search_executions');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_projects handler (bead ue3)
|
|
3
|
+
*
|
|
4
|
+
* Single tool covering both uuid-lookup and filter/list modes.
|
|
5
|
+
*
|
|
6
|
+
* Modes:
|
|
7
|
+
* uuid mode: {uuid} → {filter:{uuid}, pageInfo:{totalCount:1,...}, projects:[fullProject]}
|
|
8
|
+
* — NotFound surfaces as isError:true
|
|
9
|
+
* filter mode: {q?, page?, pageSize?} → {filter:{q}, pageInfo, projects:[summaries]}
|
|
10
|
+
*
|
|
11
|
+
* Response shape is uniform ({filter, pageInfo, projects}) but `projects[0]`
|
|
12
|
+
* richness differs by mode: uuid-mode returns the full project (all backend
|
|
13
|
+
* keys), filter-mode returns a summary (uuid/name/slug/repoName).
|
|
14
|
+
*/
|
|
15
|
+
import { Logger } from '../utils/logger.js';
|
|
16
|
+
import { handleExternalServiceError } from '../utils/errors.js';
|
|
17
|
+
import { DebuggAIServerClient } from '../services/index.js';
|
|
18
|
+
import { config } from '../config/index.js';
|
|
19
|
+
import { toPaginationParams } from '../utils/pagination.js';
|
|
20
|
+
const logger = new Logger({ module: 'searchProjectsHandler' });
|
|
21
|
+
function notFound(uuid) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify({ error: 'NotFound', message: `Project ${uuid} not found.`, uuid }, null, 2),
|
|
26
|
+
}],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function searchProjectsHandler(input, _context) {
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
logger.toolStart('search_projects', input);
|
|
33
|
+
try {
|
|
34
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
35
|
+
await client.init();
|
|
36
|
+
if (input.uuid) {
|
|
37
|
+
try {
|
|
38
|
+
const project = await client.getProject(input.uuid);
|
|
39
|
+
const payload = {
|
|
40
|
+
filter: { uuid: input.uuid },
|
|
41
|
+
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
|
|
42
|
+
projects: [project],
|
|
43
|
+
};
|
|
44
|
+
logger.toolComplete('search_projects', Date.now() - start);
|
|
45
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err?.statusCode === 404 || err?.response?.status === 404)
|
|
49
|
+
return notFound(input.uuid);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
|
|
54
|
+
const { pageInfo, projects } = await client.listProjects(pagination, input.q);
|
|
55
|
+
const payload = {
|
|
56
|
+
filter: { q: input.q ?? null },
|
|
57
|
+
pageInfo,
|
|
58
|
+
projects: projects.map((p) => ({
|
|
59
|
+
uuid: p.uuid,
|
|
60
|
+
name: p.name,
|
|
61
|
+
slug: p.slug,
|
|
62
|
+
repoName: p.repo?.name ?? null,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
logger.toolComplete('search_projects', Date.now() - start);
|
|
66
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.toolError('search_projects', error, Date.now() - start);
|
|
70
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'search_projects');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -43,16 +43,40 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
43
43
|
releaseSlot();
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
async function testPageChangesHandlerInner(input, context,
|
|
46
|
+
async function testPageChangesHandlerInner(input, context, rawProgressCallback) {
|
|
47
47
|
const startTime = Date.now();
|
|
48
48
|
logger.toolStart('check_app_in_browser', input);
|
|
49
|
+
// Bead 0bq: wrap the progress callback in a circuit-breaker so a single
|
|
50
|
+
// client-side rejection of a stale progressToken (which would normally
|
|
51
|
+
// throw up the stack and abort the handler, or — worse — arrive post-response
|
|
52
|
+
// and tear down the stdio transport) is swallowed and disables further
|
|
53
|
+
// emissions in this request.
|
|
54
|
+
let progressDisabled = false;
|
|
55
|
+
const progressCallback = rawProgressCallback
|
|
56
|
+
? async (update) => {
|
|
57
|
+
if (progressDisabled)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
await rawProgressCallback(update);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
progressDisabled = true;
|
|
64
|
+
logger.warn('Progress emission failed; disabling further emissions for this request', {
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
: undefined;
|
|
49
70
|
const client = new DebuggAIServerClient(config.api.key);
|
|
50
71
|
await client.init();
|
|
51
72
|
const originalUrl = resolveTargetUrl(input);
|
|
52
73
|
let ctx = buildContext(originalUrl);
|
|
53
74
|
let keyId;
|
|
54
75
|
const abortController = new AbortController();
|
|
55
|
-
const onStdinClose = () =>
|
|
76
|
+
const onStdinClose = () => {
|
|
77
|
+
abortController.abort();
|
|
78
|
+
progressDisabled = true; // client is gone — stop emitting
|
|
79
|
+
};
|
|
56
80
|
process.stdin.once('close', onStdinClose);
|
|
57
81
|
// Progress budget: 3 setup steps + 25 execution steps = 28 total
|
|
58
82
|
const SETUP_STEPS = 3;
|
|
@@ -168,6 +192,7 @@ async function testPageChangesHandlerInner(input, context, progressCallback) {
|
|
|
168
192
|
const BACKEND_SETUP_END = 6;
|
|
169
193
|
let lastStepsTaken = 0;
|
|
170
194
|
let observedMaxSteps = MAX_EXEC_STEPS;
|
|
195
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
171
196
|
const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
|
|
172
197
|
// Keep the tunnel alive while the workflow is actively running
|
|
173
198
|
if (ctx.tunnelId)
|
|
@@ -180,6 +205,19 @@ async function testPageChangesHandlerInner(input, context, progressCallback) {
|
|
|
180
205
|
}
|
|
181
206
|
if (!progressCallback)
|
|
182
207
|
return;
|
|
208
|
+
// Bead 0bq: emit the final "Complete:" progress INSIDE this callback
|
|
209
|
+
// when terminal status is detected. pollExecution will return on the
|
|
210
|
+
// next line (line 183 in services/workflows.ts), so there's no
|
|
211
|
+
// post-pollExecution progress emission that could race the response.
|
|
212
|
+
if (TERMINAL_STATUSES.has(exec.status)) {
|
|
213
|
+
const terminalOutcome = exec.state?.outcome ?? exec.status;
|
|
214
|
+
await progressCallback({
|
|
215
|
+
progress: TOTAL_STEPS,
|
|
216
|
+
total: TOTAL_STEPS,
|
|
217
|
+
message: `Complete: ${terminalOutcome}`,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
183
221
|
// --- Compute progress number ---
|
|
184
222
|
let execProgress;
|
|
185
223
|
let message;
|
|
@@ -308,9 +346,12 @@ async function testPageChangesHandlerInner(input, context, progressCallback) {
|
|
|
308
346
|
responsePayload.surferOutput = sanitizeResponseUrls(surferNode.outputData, ctx);
|
|
309
347
|
}
|
|
310
348
|
logger.toolComplete('check_app_in_browser', duration);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
349
|
+
// NOTE (bead 0bq): the final "Complete:" progress is emitted INSIDE
|
|
350
|
+
// pollExecution's onUpdate when terminal status is detected — see the
|
|
351
|
+
// TERMINAL_STATUSES block above. Emitting it here (post-resolve) creates
|
|
352
|
+
// a race where the progress can arrive AFTER the response on the wire,
|
|
353
|
+
// making the client reject it as an unknown progressToken and close the
|
|
354
|
+
// transport, breaking ALL subsequent tool calls.
|
|
314
355
|
// Sanitize the whole payload so no tunnel URL leaks anywhere — including
|
|
315
356
|
// agent-authored strings in actionTrace[*].intent, evaluation.reason, etc.
|
|
316
357
|
const sanitizedPayload = sanitizeResponseUrls(responsePayload, ctx);
|
|
@@ -17,16 +17,36 @@ import { DebuggAIServerClient } from '../services/index.js';
|
|
|
17
17
|
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
18
18
|
const logger = new Logger({ module: 'triggerCrawlHandler' });
|
|
19
19
|
const TEMPLATE_KEYWORD = 'raw crawl';
|
|
20
|
-
export async function triggerCrawlHandler(input, context,
|
|
20
|
+
export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
21
21
|
const startTime = Date.now();
|
|
22
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;
|
|
23
40
|
const client = new DebuggAIServerClient(config.api.key);
|
|
24
41
|
await client.init();
|
|
25
42
|
const originalUrl = resolveTargetUrl(input);
|
|
26
43
|
let ctx = buildContext(originalUrl);
|
|
27
44
|
let keyId;
|
|
28
45
|
const abortController = new AbortController();
|
|
29
|
-
const onStdinClose = () =>
|
|
46
|
+
const onStdinClose = () => {
|
|
47
|
+
abortController.abort();
|
|
48
|
+
progressDisabled = true;
|
|
49
|
+
};
|
|
30
50
|
process.stdin.once('close', onStdinClose);
|
|
31
51
|
try {
|
|
32
52
|
// --- Tunnel: reuse existing or provision a fresh one ---
|
|
@@ -93,15 +113,25 @@ export async function triggerCrawlHandler(input, context, progressCallback) {
|
|
|
93
113
|
const executionUuid = executeResponse.executionUuid;
|
|
94
114
|
logger.info(`Crawl execution queued: ${executionUuid}`);
|
|
95
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']);
|
|
96
120
|
const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
|
|
97
121
|
if (ctx.tunnelId)
|
|
98
122
|
touchTunnelById(ctx.tunnelId);
|
|
99
123
|
if (!progressCallback)
|
|
100
124
|
return;
|
|
101
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
|
+
}
|
|
102
133
|
await progressCallback({
|
|
103
|
-
progress: 4,
|
|
104
|
-
total: 4,
|
|
134
|
+
progress: 4, total: 4,
|
|
105
135
|
message: `Crawl ${exec.status} (${nodeCount} nodes)`,
|
|
106
136
|
});
|
|
107
137
|
}, abortController.signal);
|
|
@@ -154,9 +184,9 @@ export async function triggerCrawlHandler(input, context, progressCallback) {
|
|
|
154
184
|
};
|
|
155
185
|
}
|
|
156
186
|
logger.toolComplete('trigger_crawl', duration);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
160
190
|
const sanitizedPayload = sanitizeResponseUrls(responsePayload, ctx);
|
|
161
191
|
return {
|
|
162
192
|
content: [{ type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) }],
|
|
@@ -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);
|
package/dist/services/index.js
CHANGED
|
@@ -191,7 +191,7 @@ export class DebuggAIServerClient {
|
|
|
191
191
|
* List environments for a project. Paginated.
|
|
192
192
|
* Optional q filters by name via backend ?search=.
|
|
193
193
|
* The bare-array variant (no pagination) is still used internally by
|
|
194
|
-
*
|
|
194
|
+
* search_environments when iterating across all envs to inline credentials.
|
|
195
195
|
*/
|
|
196
196
|
async listEnvironmentsForProject(projectUuid, q) {
|
|
197
197
|
if (!this.tx)
|
|
@@ -298,8 +298,8 @@ export class DebuggAIServerClient {
|
|
|
298
298
|
/**
|
|
299
299
|
* List credentials for a specific environment. Unpaginated (fetches up to
|
|
300
300
|
* backend max pageSize). q filters label/username server-side via ?search=;
|
|
301
|
-
* role filters server-side. Used internally by
|
|
302
|
-
*
|
|
301
|
+
* role filters server-side. Used internally by search_environments when
|
|
302
|
+
* inlining credentials on each env in a page.
|
|
303
303
|
*/
|
|
304
304
|
async listCredentialsForEnvironment(projectUuid, envUuid, q, role) {
|
|
305
305
|
if (!this.tx)
|
|
@@ -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',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateProjectInputSchema } from '../types/index.js';
|
|
2
2
|
import { createProjectHandler } from '../handlers/createProjectHandler.js';
|
|
3
|
-
const DESCRIPTION = `Create a new DebuggAI project. Required: name, platform (e.g. "web"), teamUuid
|
|
3
|
+
const DESCRIPTION = `Create a new DebuggAI project. Required: name, platform (e.g. "web"), plus a team and a repo. Team and repo each accept EITHER a UUID or a name: pass teamUuid OR teamName, and repoUuid OR repoName. Name resolution does a backend search with case-insensitive exact match (returns AmbiguousMatch with candidates on multiple hits, NotFound on no hit). The repo must be GitHub-linked to the account. Returns {created: true, project: {uuid, name, slug, platform, repoName, ...}}.`;
|
|
4
4
|
export function buildCreateProjectTool() {
|
|
5
5
|
return {
|
|
6
6
|
name: 'create_project',
|
|
@@ -11,10 +11,12 @@ export function buildCreateProjectTool() {
|
|
|
11
11
|
properties: {
|
|
12
12
|
name: { type: 'string', description: 'Project name. Required.', minLength: 1 },
|
|
13
13
|
platform: { type: 'string', description: 'Platform (e.g. "web"). Required.', minLength: 1 },
|
|
14
|
-
teamUuid: { type: 'string', description: 'Team UUID
|
|
15
|
-
|
|
14
|
+
teamUuid: { type: 'string', description: 'Team UUID. Provide teamUuid OR teamName, not both.' },
|
|
15
|
+
teamName: { type: 'string', description: 'Team name (backend-resolved, case-insensitive exact match). Provide teamUuid OR teamName, not both.' },
|
|
16
|
+
repoUuid: { type: 'string', description: 'GitHub repo UUID. Provide repoUuid OR repoName, not both. Repo must be GitHub-linked.' },
|
|
17
|
+
repoName: { type: 'string', description: 'GitHub repo name, e.g. "org/repo" (backend-resolved, case-insensitive exact match). Provide repoUuid OR repoName, not both.' },
|
|
16
18
|
},
|
|
17
|
-
required: ['name', 'platform'
|
|
19
|
+
required: ['name', 'platform'],
|
|
18
20
|
additionalProperties: false,
|
|
19
21
|
},
|
|
20
22
|
};
|
package/dist/tools/index.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { buildTestPageChangesTool, buildValidatedTestPageChangesTool } from './testPageChanges.js';
|
|
2
2
|
import { buildTriggerCrawlTool, buildValidatedTriggerCrawlTool } from './triggerCrawl.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { buildSearchProjectsTool, buildValidatedSearchProjectsTool } from './searchProjects.js';
|
|
4
|
+
import { buildSearchEnvironmentsTool, buildValidatedSearchEnvironmentsTool } from './searchEnvironments.js';
|
|
5
|
+
import { buildSearchExecutionsTool, buildValidatedSearchExecutionsTool } from './searchExecutions.js';
|
|
6
6
|
import { buildCreateEnvironmentTool, buildValidatedCreateEnvironmentTool } from './createEnvironment.js';
|
|
7
|
-
import { buildCreateCredentialTool, buildValidatedCreateCredentialTool } from './createCredential.js';
|
|
8
|
-
import { buildGetEnvironmentTool, buildValidatedGetEnvironmentTool } from './getEnvironment.js';
|
|
9
7
|
import { buildUpdateEnvironmentTool, buildValidatedUpdateEnvironmentTool } from './updateEnvironment.js';
|
|
10
8
|
import { buildDeleteEnvironmentTool, buildValidatedDeleteEnvironmentTool } from './deleteEnvironment.js';
|
|
11
|
-
import { buildGetCredentialTool, buildValidatedGetCredentialTool } from './getCredential.js';
|
|
12
|
-
import { buildUpdateCredentialTool, buildValidatedUpdateCredentialTool } from './updateCredential.js';
|
|
13
|
-
import { buildDeleteCredentialTool, buildValidatedDeleteCredentialTool } from './deleteCredential.js';
|
|
14
|
-
import { buildGetProjectTool, buildValidatedGetProjectTool } from './getProject.js';
|
|
15
9
|
import { buildUpdateProjectTool, buildValidatedUpdateProjectTool } from './updateProject.js';
|
|
16
10
|
import { buildDeleteProjectTool, buildValidatedDeleteProjectTool } from './deleteProject.js';
|
|
17
|
-
import { buildListExecutionsTool, buildValidatedListExecutionsTool } from './listExecutions.js';
|
|
18
|
-
import { buildGetExecutionTool, buildValidatedGetExecutionTool } from './getExecution.js';
|
|
19
|
-
import { buildCancelExecutionTool, buildValidatedCancelExecutionTool } from './cancelExecution.js';
|
|
20
|
-
import { buildListTeamsTool, buildValidatedListTeamsTool } from './listTeams.js';
|
|
21
|
-
import { buildListReposTool, buildValidatedListReposTool } from './listRepos.js';
|
|
22
11
|
import { buildCreateProjectTool, buildValidatedCreateProjectTool } from './createProject.js';
|
|
23
12
|
let _tools = null;
|
|
24
13
|
let _validatedTools = null;
|
|
@@ -30,49 +19,27 @@ export function initTools(ctx) {
|
|
|
30
19
|
const tools = [
|
|
31
20
|
buildTestPageChangesTool(ctx),
|
|
32
21
|
buildTriggerCrawlTool(ctx),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
buildListCredentialsTool(),
|
|
22
|
+
buildSearchProjectsTool(),
|
|
23
|
+
buildSearchEnvironmentsTool(),
|
|
36
24
|
buildCreateEnvironmentTool(),
|
|
37
|
-
buildCreateCredentialTool(),
|
|
38
|
-
buildGetEnvironmentTool(),
|
|
39
25
|
buildUpdateEnvironmentTool(),
|
|
40
26
|
buildDeleteEnvironmentTool(),
|
|
41
|
-
buildGetCredentialTool(),
|
|
42
|
-
buildUpdateCredentialTool(),
|
|
43
|
-
buildDeleteCredentialTool(),
|
|
44
|
-
buildGetProjectTool(),
|
|
45
27
|
buildUpdateProjectTool(),
|
|
46
28
|
buildDeleteProjectTool(),
|
|
47
|
-
|
|
48
|
-
buildGetExecutionTool(),
|
|
49
|
-
buildCancelExecutionTool(),
|
|
50
|
-
buildListTeamsTool(),
|
|
51
|
-
buildListReposTool(),
|
|
29
|
+
buildSearchExecutionsTool(),
|
|
52
30
|
buildCreateProjectTool(),
|
|
53
31
|
];
|
|
54
32
|
const validated = [
|
|
55
33
|
buildValidatedTestPageChangesTool(ctx),
|
|
56
34
|
buildValidatedTriggerCrawlTool(ctx),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
buildValidatedListCredentialsTool(),
|
|
35
|
+
buildValidatedSearchProjectsTool(),
|
|
36
|
+
buildValidatedSearchEnvironmentsTool(),
|
|
60
37
|
buildValidatedCreateEnvironmentTool(),
|
|
61
|
-
buildValidatedCreateCredentialTool(),
|
|
62
|
-
buildValidatedGetEnvironmentTool(),
|
|
63
38
|
buildValidatedUpdateEnvironmentTool(),
|
|
64
39
|
buildValidatedDeleteEnvironmentTool(),
|
|
65
|
-
buildValidatedGetCredentialTool(),
|
|
66
|
-
buildValidatedUpdateCredentialTool(),
|
|
67
|
-
buildValidatedDeleteCredentialTool(),
|
|
68
|
-
buildValidatedGetProjectTool(),
|
|
69
40
|
buildValidatedUpdateProjectTool(),
|
|
70
41
|
buildValidatedDeleteProjectTool(),
|
|
71
|
-
|
|
72
|
-
buildValidatedGetExecutionTool(),
|
|
73
|
-
buildValidatedCancelExecutionTool(),
|
|
74
|
-
buildValidatedListTeamsTool(),
|
|
75
|
-
buildValidatedListReposTool(),
|
|
42
|
+
buildValidatedSearchExecutionsTool(),
|
|
76
43
|
buildValidatedCreateProjectTool(),
|
|
77
44
|
];
|
|
78
45
|
_tools = tools;
|