@debugg-ai/debugg-ai-mcp
Version:
Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.
133 lines (132 loc) • 5.95 kB
JavaScript
/**
* search_environments handler (bead 5kw)
*
* Absorbs list_environments + get_environment + all credential search.
* Each environment in the response has its credentials expanded inline.
*
* Modes:
* uuid mode: {uuid, projectUuid?} → {filter:{uuid}, project, pageInfo:{totalCount:1,...},
* environments:[{...env, credentials:[...]}]}
* filter mode: {projectUuid?, q?, page?, pageSize?} → paginated list, creds inline per env
*
* Invariants:
* - NEVER returns a password field anywhere in the response (defensive strip at handler edge)
* - Git-fallback for projectUuid: detectRepoName() → findProjectByRepoName(); NoProjectResolved if both fail
* - NotFound on unknown uuid returns isError:true
*/
import { Logger } from '../utils/logger.js';
import { handleExternalServiceError } from '../utils/errors.js';
import { DebuggAIServerClient } from '../services/index.js';
import { config } from '../config/index.js';
import { detectRepoName } from '../utils/gitContext.js';
import { toPaginationParams, makePageInfo } from '../utils/pagination.js';
const logger = new Logger({ module: 'searchEnvironmentsHandler' });
function stripPassword(cred) {
// Defensive: take only known-safe keys. Never spread the source.
return {
uuid: cred.uuid,
label: cred.label,
username: cred.username,
role: cred.role ?? null,
...(cred.environmentUuid ? { environmentUuid: cred.environmentUuid } : {}),
};
}
function notFound(uuid) {
return {
content: [{
type: 'text',
text: JSON.stringify({ error: 'NotFound', message: `Environment ${uuid} not found.`, uuid }, null, 2),
}],
isError: true,
};
}
function noProjectResolved(pagination, reason) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: 'NoProjectResolved',
message: reason,
pageInfo: makePageInfo(pagination.page, pagination.pageSize, 0, null),
environments: [],
}, null, 2),
}],
};
}
export async function searchEnvironmentsHandler(input, _context) {
const start = Date.now();
const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
logger.toolStart('search_environments', { ...input, ...pagination });
try {
const client = new DebuggAIServerClient(config.api.key);
await client.init();
// ── Resolve projectUuid ──
// Bead gb4n: when projectUuid is provided directly (caller skips git
// auto-resolution), `name` and `repoName` are unknown. OMIT those fields
// rather than emitting nulls — null fields surprised callers and
// muddied the contract. If a caller needs them, they fetch via
// search_projects.
let projectUuid = input.projectUuid;
let project = null;
if (!projectUuid) {
const repoName = detectRepoName();
if (!repoName) {
return noProjectResolved(pagination, 'No git repo detected and no projectUuid provided. Pass projectUuid (get via search_projects) or invoke from a directory with a git origin.');
}
const resolved = await client.findProjectByRepoName(repoName);
if (!resolved) {
return noProjectResolved(pagination, `No DebuggAI project found for repo "${repoName}". Pass projectUuid explicitly.`);
}
projectUuid = resolved.uuid;
project = { uuid: resolved.uuid };
if (resolved.name)
project.name = resolved.name;
const rn = resolved.repo?.name ?? repoName;
if (rn)
project.repoName = rn;
}
else {
project = { uuid: projectUuid };
}
// ── uuid mode ──
if (input.uuid) {
try {
const env = await client.getEnvironment(projectUuid, input.uuid);
const creds = await client.listCredentialsForEnvironment(projectUuid, input.uuid).catch(() => []);
const payload = {
project,
filter: { uuid: input.uuid },
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
environments: [{ ...env, credentials: creds.map(stripPassword) }],
};
logger.toolComplete('search_environments', Date.now() - start);
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
}
catch (err) {
if (err?.statusCode === 404 || err?.response?.status === 404)
return notFound(input.uuid);
throw err;
}
}
// ── Filter mode ──
const { pageInfo, environments } = await client.listEnvironmentsPaginated(projectUuid, pagination, input.q);
// Expand creds per env (sequential — bounded by page size, typically ≤20)
const withCreds = [];
for (const env of environments) {
const creds = await client.listCredentialsForEnvironment(projectUuid, env.uuid).catch(() => []);
withCreds.push({ ...env, credentials: creds.map(stripPassword) });
}
const payload = {
project,
filter: { q: input.q ?? null },
pageInfo,
environments: withCreds,
};
logger.toolComplete('search_environments', Date.now() - start);
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
}
catch (error) {
logger.toolError('search_environments', error, Date.now() - start);
throw handleExternalServiceError(error, 'DebuggAI', 'search_environments');
}
}