@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +49 -37
  3. package/dist/config/index.js +4 -1
  4. package/dist/handlers/createEnvironmentHandler.js +34 -1
  5. package/dist/handlers/createProjectHandler.js +62 -10
  6. package/dist/handlers/index.js +4 -14
  7. package/dist/handlers/searchEnvironmentsHandler.js +122 -0
  8. package/dist/handlers/searchExecutionsHandler.js +71 -0
  9. package/dist/handlers/searchProjectsHandler.js +72 -0
  10. package/dist/handlers/testPageChangesHandler.js +46 -5
  11. package/dist/handlers/triggerCrawlHandler.js +37 -7
  12. package/dist/handlers/updateEnvironmentHandler.js +94 -15
  13. package/dist/index.js +15 -2
  14. package/dist/services/index.js +3 -3
  15. package/dist/tools/createEnvironment.js +5 -1
  16. package/dist/tools/createProject.js +6 -4
  17. package/dist/tools/index.js +9 -42
  18. package/dist/tools/searchEnvironments.js +35 -0
  19. package/dist/tools/searchExecutions.js +31 -0
  20. package/dist/tools/searchProjects.js +30 -0
  21. package/dist/types/index.js +52 -71
  22. package/package.json +8 -2
  23. package/dist/handlers/cancelExecutionHandler.js +0 -41
  24. package/dist/handlers/createCredentialHandler.js +0 -60
  25. package/dist/handlers/deleteCredentialHandler.js +0 -51
  26. package/dist/handlers/getCredentialHandler.js +0 -49
  27. package/dist/handlers/getEnvironmentHandler.js +0 -49
  28. package/dist/handlers/getExecutionHandler.js +0 -37
  29. package/dist/handlers/getProjectHandler.js +0 -37
  30. package/dist/handlers/listCredentialsHandler.js +0 -93
  31. package/dist/handlers/listEnvironmentsHandler.js +0 -63
  32. package/dist/handlers/listExecutionsHandler.js +0 -35
  33. package/dist/handlers/listProjectsHandler.js +0 -32
  34. package/dist/handlers/listReposHandler.js +0 -27
  35. package/dist/handlers/listTeamsHandler.js +0 -27
  36. package/dist/handlers/updateCredentialHandler.js +0 -70
  37. package/dist/tools/cancelExecution.js +0 -22
  38. package/dist/tools/createCredential.js +0 -52
  39. package/dist/tools/deleteCredential.js +0 -24
  40. package/dist/tools/getCredential.js +0 -24
  41. package/dist/tools/getEnvironment.js +0 -23
  42. package/dist/tools/getExecution.js +0 -22
  43. package/dist/tools/getProject.js +0 -22
  44. package/dist/tools/listCredentials.js +0 -30
  45. package/dist/tools/listEnvironments.js +0 -28
  46. package/dist/tools/listExecutions.js +0 -24
  47. package/dist/tools/listProjects.js +0 -27
  48. package/dist/tools/listRepos.js +0 -23
  49. package/dist/tools/listTeams.js +0 -23
  50. 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, progressCallback) {
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 = () => abortController.abort();
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
- if (progressCallback) {
312
- await progressCallback({ progress: TOTAL_STEPS, total: TOTAL_STEPS, message: `Complete: ${outcome}` });
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, progressCallback) {
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 = () => abortController.abort();
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
- if (progressCallback) {
158
- await progressCallback({ progress: 4, total: 4, message: `Crawl ${finalExecution.status}` });
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
- patchKeys: Object.keys(input).filter(k => k !== 'uuid' && k !== 'projectUuid'),
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
- try {
38
- const environment = await client.updateEnvironment(projectUuid, input.uuid, {
39
- name: input.name,
40
- url: input.url,
41
- description: input.description,
42
- });
43
- logger.toolComplete('update_environment', Date.now() - start);
44
- return {
45
- content: [{ type: 'text', text: JSON.stringify({ updated: true, environment }, null, 2) }],
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
- catch (err) {
49
- if (err?.statusCode === 404 || err?.response?.status === 404) {
50
- return notFound(input.uuid, `backend returned 404 for project ${projectUuid}`);
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
- // Validate required environment variables
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
- throw new Error('Missing required environment variable: DEBUGGAI_API_KEY');
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);
@@ -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
- * list_credentials when iterating across all envs.
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 list_credentials when
302
- * iterating across envs.
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 list_projects). Returns the created environment's uuid so you can reference it when running check_app_in_browser or creating credentials.`;
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 (from list_teams), repoUuid (from list_repos). Returns {created: true, project: {uuid, name, slug, platform, repoName, ...}}. The repo must be GitHub-linked to the account. Use list_teams + list_repos first to discover valid UUIDs.`;
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 (from list_teams). Required.' },
15
- repoUuid: { type: 'string', description: 'GitHub repo UUID (from list_repos). Required repo must be GitHub-linked.' },
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', 'teamUuid', 'repoUuid'],
19
+ required: ['name', 'platform'],
18
20
  additionalProperties: false,
19
21
  },
20
22
  };
@@ -1,24 +1,13 @@
1
1
  import { buildTestPageChangesTool, buildValidatedTestPageChangesTool } from './testPageChanges.js';
2
2
  import { buildTriggerCrawlTool, buildValidatedTriggerCrawlTool } from './triggerCrawl.js';
3
- import { buildListEnvironmentsTool, buildValidatedListEnvironmentsTool } from './listEnvironments.js';
4
- import { buildListCredentialsTool, buildValidatedListCredentialsTool } from './listCredentials.js';
5
- import { buildListProjectsTool, buildValidatedListProjectsTool } from './listProjects.js';
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
- buildListProjectsTool(),
34
- buildListEnvironmentsTool(),
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
- buildListExecutionsTool(),
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
- buildValidatedListProjectsTool(),
58
- buildValidatedListEnvironmentsTool(),
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
- buildValidatedListExecutionsTool(),
72
- buildValidatedGetExecutionTool(),
73
- buildValidatedCancelExecutionTool(),
74
- buildValidatedListTeamsTool(),
75
- buildValidatedListReposTool(),
42
+ buildValidatedSearchExecutionsTool(),
76
43
  buildValidatedCreateProjectTool(),
77
44
  ];
78
45
  _tools = tools;