import { z } from "zod";
import { apiRequest } from "../utils/api-client.js";
import { McpError, ErrorCode, checkProAccess } from "../utils/errors.js";
import { debug } from "../utils/logger.js";
// ============================================================================
// SCHEMAS
// ============================================================================
/**
* Input schema for create_audit tool.
*/
export const CreateAuditInputSchema = z.object({
name: z.string().min(1).max(200).describe("Name for the audit report"),
technologies: z
.array(
z.object({
name: z
.string()
.min(1)
.describe('Technology name (e.g., "react", "express", "lodash")'),
version: z
.string()
.optional()
.describe('Version string (e.g., "18.2.0", "4.17.21")'),
category: z
.string()
.optional()
.describe('Optional category (e.g., "frontend", "backend")'),
}),
)
.min(1)
.max(50)
.describe("List of technologies to audit"),
});
export type CreateAuditInput = z.infer<typeof CreateAuditInputSchema>;
/**
* Input schema for get_audit tool.
*/
export const GetAuditInputSchema = z.object({
auditId: z.string().uuid().describe("Audit report UUID"),
});
export type GetAuditInput = z.infer<typeof GetAuditInputSchema>;
/**
* Input schema for list_audits tool.
*/
export const ListAuditsInputSchema = z.object({
limit: z
.number()
.min(1)
.max(50)
.optional()
.default(10)
.describe("Max results to return"),
offset: z.number().min(0).optional().default(0).describe("Pagination offset"),
});
export type ListAuditsInput = z.infer<typeof ListAuditsInputSchema>;
/**
* Input schema for compare_audits tool.
*/
export const CompareAuditsInputSchema = z.object({
baseAuditId: z.string().uuid().describe("Base audit ID (older)"),
compareAuditId: z.string().uuid().describe("Compare audit ID (newer)"),
});
export type CompareAuditsInput = z.infer<typeof CompareAuditsInputSchema>;
/**
* Input schema for get_audit_quota tool.
*/
export const GetAuditQuotaInputSchema = z.object({});
export type GetAuditQuotaInput = z.infer<typeof GetAuditQuotaInputSchema>;
/**
* Input schema for get_migration_recommendation tool.
*/
export const GetMigrationRecommendationInputSchema = z.object({
auditId: z
.string()
.uuid()
.describe("Audit report UUID to analyze for migration"),
});
export type GetMigrationRecommendationInput = z.infer<
typeof GetMigrationRecommendationInputSchema
>;
// ============================================================================
// TOOL DEFINITIONS
// ============================================================================
export const createAuditToolDefinition = {
name: "create_audit",
description: `Create a technical debt audit for your tech stack. Analyzes for deprecated packages, security vulnerabilities, EOL versions, and upgrade recommendations.
**Tier**: Requires Pro or Team subscription (OR OAuth session)
**Prerequisites**:
- Pro/Team account or authenticated via OAuth
- List of technologies with versions (use package.json data)
**Next Steps**:
- Get full report: \`get_audit({ auditId: "returned-uuid" })\`
- Get migration plan: \`get_migration_recommendation({ auditId: "uuid" })\`
- Compare over time: \`compare_audits({ baseAuditId, compareAuditId })\`
**Output includes**:
- Health score (0-100)
- Findings by severity (critical/high/medium/low/info)
- Actionable upgrade recommendations
- CVE detection for known vulnerabilities
**Common Pitfalls**:
- Include version numbers for accurate vulnerability detection
- Use technology names as they appear in package managers
**Example**: \`create_audit({ name: "Q1 2026 Stack Review", technologies: [{ name: "React", version: "17.0.0" }, { name: "Node.js", version: "14.0.0" }] })\``,
inputSchema: {
type: "object" as const,
properties: {
name: {
type: "string",
description: 'Name for the audit report (e.g., "Q1 2026 Stack Review")',
},
technologies: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "Technology name" },
version: { type: "string", description: "Version (optional)" },
category: { type: "string", description: "Category (optional)" },
},
required: ["name"],
},
description: "Technologies to audit",
},
},
required: ["name", "technologies"],
},
};
export const getAuditToolDefinition = {
name: "get_audit",
description:
"Fetch a completed audit report by ID. Returns all findings and health score.",
inputSchema: {
type: "object" as const,
properties: {
auditId: {
type: "string",
format: "uuid",
description: "Audit report UUID",
},
},
required: ["auditId"],
},
};
export const listAuditsToolDefinition = {
name: "list_audits",
description:
"List your audit reports with pagination. Shows name, status, health score, and creation date.",
inputSchema: {
type: "object" as const,
properties: {
limit: {
type: "number",
description: "Max results (1-50, default 10)",
},
offset: {
type: "number",
description: "Pagination offset (default 0)",
},
},
required: [],
},
};
export const compareAuditsToolDefinition = {
name: "compare_audits",
description: `Compare two audit reports to track technical debt trends over time.
Shows new issues introduced, issues resolved, and health score change.
Useful for measuring progress on debt reduction.`,
inputSchema: {
type: "object" as const,
properties: {
baseAuditId: {
type: "string",
format: "uuid",
description: "Base (older) audit ID",
},
compareAuditId: {
type: "string",
format: "uuid",
description: "Compare (newer) audit ID",
},
},
required: ["baseAuditId", "compareAuditId"],
},
};
export const getAuditQuotaToolDefinition = {
name: "get_audit_quota",
description: "Check your remaining audit quota for this month.",
inputSchema: {
type: "object" as const,
properties: {},
required: [],
},
};
export const getMigrationRecommendationToolDefinition = {
name: "get_migration_recommendation",
description: `Analyze an audit report for migration opportunities.
Returns a detailed migration recommendation including:
- Technologies that should be replaced
- Recommended modern alternatives
- Migration roadmap with phases
- Risk assessment
- Builder constraints to pre-fill for generating a migration blueprint
Use this after create_audit to get actionable migration guidance.`,
inputSchema: {
type: "object" as const,
properties: {
auditId: {
type: "string",
format: "uuid",
description: "Audit report UUID to analyze",
},
},
required: ["auditId"],
},
};
// ============================================================================
// API RESPONSE TYPES
// ============================================================================
interface AuditFinding {
id: string;
category: string;
severity: "critical" | "high" | "medium" | "low" | "info";
title: string;
description: string;
technology: string;
currentVersion?: string;
recommendedVersion?: string;
eolDate?: string;
cveIds?: string[];
migrationEffort?: string;
suggestedAction: string;
references?: string[];
autoFixable?: boolean;
}
interface AuditSummary {
totalFindings: number;
criticalCount: number;
highCount: number;
mediumCount: number;
lowCount: number;
infoCount: number;
healthScore: number;
}
interface AuditResponse {
id: string;
name: string;
status: string;
findings?: AuditFinding[];
summary?: AuditSummary;
createdAt: string;
completedAt?: string;
}
interface AuditListResponse {
audits: Array<{
id: string;
name: string;
status: string;
summary?: AuditSummary;
createdAt: string;
}>;
total: number;
}
interface CompareResponse {
comparison: {
baseAudit: { id: string; name: string; healthScore: number };
compareAudit: { id: string; name: string; healthScore: number };
healthScoreDelta: number;
trend: "improving" | "stable" | "degrading";
newFindings: AuditFinding[];
resolvedFindings: AuditFinding[];
newCount: number;
resolvedCount: number;
};
}
interface QuotaResponse {
quota: {
used: number;
limit: number | "unlimited";
remaining: number | "unlimited";
resetsAt: string | null;
};
}
interface MigrationRecommendationResponse {
auditId: string;
needsMigration: boolean;
migrationScore: number;
affectedTechCount: number;
criticalIssues: number;
recommendation: {
id: string;
urgency: "critical" | "high" | "medium" | "low";
scope: "full-stack" | "partial" | "incremental";
title: string;
summary: string;
estimatedEffort: string;
techsToReplace: Array<{
techId: string;
name: string;
currentVersion?: string;
reason: string;
relatedFindings: string[];
}>;
suggestedAlternatives: Array<{
forTech: string;
alternatives: string[];
preferredChoice?: string;
reason: string;
}>;
inferredConstraints: string[];
inferredContext: {
projectType?: string;
scale?: string;
priorities: string[];
};
migrationSteps: Array<{
order: number;
phase: string;
description: string;
techsAffected: string[];
effort: string;
}>;
risks: Array<{
level: "high" | "medium" | "low";
description: string;
mitigation: string;
}>;
} | null;
builderPreFill: {
constraintIds: string[];
context: {
projectType?: string;
scale?: string;
priorities: string[];
};
hints: {
techsToAvoid: string[];
preferredAlternatives: Record<string, string>;
};
migrationMode: boolean;
} | null;
}
// ============================================================================
// FORMATTERS
// ============================================================================
function formatSeverityMarker(severity: string): string {
switch (severity) {
case "critical":
return "[CRITICAL]";
case "high":
return "[HIGH]";
case "medium":
return "[MEDIUM]";
case "low":
return "[LOW]";
case "info":
return "[INFO]";
default:
return "[-]";
}
}
function formatHealthScore(score: number): string {
if (score >= 90) return `[EXCELLENT] ${score}/100`;
if (score >= 70) return `[GOOD] ${score}/100`;
if (score >= 50) return `[FAIR] ${score}/100`;
return `[NEEDS ATTENTION] ${score}/100`;
}
function formatAuditReport(audit: AuditResponse): string {
let text = `## Audit Report: ${audit.name}\n\n`;
text += `**ID**: ${audit.id}\n`;
text += `**Status**: ${audit.status}\n`;
text += `**Created**: ${new Date(audit.createdAt).toLocaleDateString()}\n`;
if (audit.summary) {
text += `\n### Health Score\n`;
text += `${formatHealthScore(audit.summary.healthScore)}\n\n`;
text += `### Summary\n`;
text += `| Severity | Count |\n`;
text += `|----------|-------|\n`;
text += `| Critical | ${audit.summary.criticalCount} |\n`;
text += `| High | ${audit.summary.highCount} |\n`;
text += `| Medium | ${audit.summary.mediumCount} |\n`;
text += `| Low | ${audit.summary.lowCount} |\n`;
text += `| Info | ${audit.summary.infoCount} |\n`;
text += `\n**Total Findings**: ${audit.summary.totalFindings}\n`;
}
if (audit.findings && audit.findings.length > 0) {
text += `\n### Findings\n\n`;
for (const finding of audit.findings) {
text += `#### ${formatSeverityMarker(finding.severity)} ${finding.title}\n`;
text += `**Technology**: ${finding.technology}`;
if (finding.currentVersion) {
text += ` (v${finding.currentVersion})`;
}
text += `\n`;
text += `**Category**: ${finding.category}\n`;
text += `**Severity**: ${finding.severity.toUpperCase()}\n\n`;
text += `${finding.description}\n\n`;
if (finding.recommendedVersion) {
text += `**Recommended**: Upgrade to v${finding.recommendedVersion}\n`;
}
if (finding.migrationEffort) {
text += `**Migration Effort**: ${finding.migrationEffort}\n`;
}
if (finding.cveIds && finding.cveIds.length > 0) {
text += `**CVEs**: ${finding.cveIds.join(", ")}\n`;
}
text += `\n**Action**: ${finding.suggestedAction}\n\n`;
if (finding.references && finding.references.length > 0) {
text += `**References**:\n`;
for (const ref of finding.references) {
text += `- ${ref}\n`;
}
}
text += `---\n\n`;
}
} else if (audit.status === "completed") {
text += `\n### All Clear\n`;
text += `Your stack passed all checks. Great job maintaining your technical health!\n`;
}
return text;
}
function formatComparison(comparison: CompareResponse["comparison"]): string {
let text = `## Audit Comparison\n\n`;
text += `### Overview\n`;
text += `| Audit | Health Score |\n`;
text += `|-------|-------------|\n`;
text += `| ${comparison.baseAudit.name} (base) | ${comparison.baseAudit.healthScore}/100 |\n`;
text += `| ${comparison.compareAudit.name} (compare) | ${comparison.compareAudit.healthScore}/100 |\n`;
text += `\n`;
const trendMarker =
comparison.trend === "improving"
? "[UP]"
: comparison.trend === "degrading"
? "[DOWN]"
: "[STABLE]";
text += `### Trend: ${trendMarker} ${comparison.trend.toUpperCase()}\n`;
text += `**Health Score Change**: ${comparison.healthScoreDelta > 0 ? "+" : ""}${comparison.healthScoreDelta} points\n\n`;
if (comparison.resolvedCount > 0) {
text += `### Resolved Issues (${comparison.resolvedCount})\n`;
for (const finding of comparison.resolvedFindings) {
text += `- ${formatSeverityMarker(finding.severity)} ${finding.title} (${finding.technology})\n`;
}
text += `\n`;
}
if (comparison.newCount > 0) {
text += `### New Issues (${comparison.newCount})\n`;
for (const finding of comparison.newFindings) {
text += `- ${formatSeverityMarker(finding.severity)} ${finding.title} (${finding.technology})\n`;
}
text += `\n`;
}
if (comparison.resolvedCount === 0 && comparison.newCount === 0) {
text += `### No Changes\n`;
text += `The findings are identical between these two audits.\n`;
}
return text;
}
function formatMigrationRecommendation(
response: MigrationRecommendationResponse,
): string {
if (!response.needsMigration) {
return `## Migration Analysis\n\n**No migration recommended.**\n\nYour stack is healthy with no critical issues requiring migration.\n\n**Migration Score**: ${response.migrationScore}/100 (below threshold)`;
}
const rec = response.recommendation;
if (!rec) {
return `## Migration Analysis\n\nUnable to generate recommendation.`;
}
const urgencyMarker = {
critical: "[CRITICAL]",
high: "[HIGH]",
medium: "[MEDIUM]",
low: "[LOW]",
}[rec.urgency];
let text = `## ${urgencyMarker} Migration Recommendation\n\n`;
text += `### ${rec.title}\n\n`;
text += `**Migration Score**: ${response.migrationScore}/100\n`;
text += `**Urgency**: ${rec.urgency.toUpperCase()}\n`;
text += `**Scope**: ${rec.scope}\n`;
text += `**Estimated Effort**: ${rec.estimatedEffort}\n`;
text += `**Affected Technologies**: ${response.affectedTechCount}\n`;
text += `**Critical Issues**: ${response.criticalIssues}\n\n`;
text += `${rec.summary}\n\n`;
// Technologies to Replace
if (rec.techsToReplace.length > 0) {
text += `### Technologies to Replace\n\n`;
text += `| Technology | Version | Reason |\n`;
text += `|------------|---------|--------|\n`;
for (const tech of rec.techsToReplace) {
text += `| ${tech.name} | ${tech.currentVersion || "-"} | ${tech.reason} |\n`;
}
text += `\n`;
}
// Suggested Alternatives
if (rec.suggestedAlternatives.length > 0) {
text += `### Recommended Alternatives\n\n`;
for (const alt of rec.suggestedAlternatives) {
text += `**${alt.forTech}** → `;
const alts = alt.alternatives.map((a) =>
a === alt.preferredChoice ? `**${a}** (recommended)` : a,
);
text += alts.join(", ");
text += `\n`;
text += ` _${alt.reason}_\n\n`;
}
}
// Migration Roadmap
if (rec.migrationSteps.length > 0) {
text += `### Migration Roadmap\n\n`;
for (const step of rec.migrationSteps) {
text += `**Phase ${step.order}: ${step.phase}** (${step.effort} effort)\n`;
text += `${step.description}\n`;
if (step.techsAffected.length > 0) {
text += `Affects: ${step.techsAffected.join(", ")}\n`;
}
text += `\n`;
}
}
// Risks
if (rec.risks.length > 0) {
text += `### Risk Assessment\n\n`;
for (const risk of rec.risks) {
const riskMarker =
risk.level === "high"
? "[HIGH]"
: risk.level === "medium"
? "[MEDIUM]"
: "[LOW]";
text += `${riskMarker} **${risk.level.toUpperCase()} RISK**: ${risk.description}\n`;
text += ` _Mitigation_: ${risk.mitigation}\n\n`;
}
}
// Inferred Constraints
if (rec.inferredConstraints.length > 0) {
text += `### Inferred Builder Constraints\n\n`;
text += `These constraints will be pre-filled when generating a migration blueprint:\n`;
text += rec.inferredConstraints.map((c) => `\`${c}\``).join(", ");
text += `\n\n`;
}
// Builder Pre-fill info
if (response.builderPreFill) {
text += `### Generate Migration Blueprint\n\n`;
text += `Use the StacksFinder Builder with these pre-filled settings:\n`;
if (response.builderPreFill.context.projectType) {
text += `- **Project Type**: ${response.builderPreFill.context.projectType}\n`;
}
if (response.builderPreFill.context.scale) {
text += `- **Scale**: ${response.builderPreFill.context.scale}\n`;
}
if (response.builderPreFill.context.priorities.length > 0) {
text += `- **Priorities**: ${response.builderPreFill.context.priorities.join(", ")}\n`;
}
if (response.builderPreFill.hints.techsToAvoid.length > 0) {
text += `- **Avoid**: ${response.builderPreFill.hints.techsToAvoid.join(", ")}\n`;
}
text += `\nVisit https://stacksfinder.com/builder to generate your migration blueprint.\n`;
}
return text;
}
// ============================================================================
// EXECUTE FUNCTIONS
// ============================================================================
/**
* Execute create_audit tool.
*/
export async function executeCreateAudit(
input: CreateAuditInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("create_audit");
if (tierCheck) return tierCheck;
debug("Creating audit", {
name: input.name,
techCount: input.technologies.length,
});
try {
const response = await apiRequest<AuditResponse>("/api/v1/audits", {
method: "POST",
body: {
name: input.name,
stackInput: {
technologies: input.technologies,
},
source: "mcp",
},
timeoutMs: 30000,
});
const text = formatAuditReport(response);
return { text };
} catch (err) {
if (err instanceof McpError) {
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error ? err.message : "Failed to create audit",
);
return { text: error.toResponseText(), isError: true };
}
}
/**
* Execute get_audit tool.
*/
export async function executeGetAudit(
input: GetAuditInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("get_audit");
if (tierCheck) return tierCheck;
debug("Fetching audit", { auditId: input.auditId });
try {
const response = await apiRequest<AuditResponse>(
`/api/v1/audits/${input.auditId}`,
);
const text = formatAuditReport(response);
return { text };
} catch (err) {
if (err instanceof McpError) {
if (err.code === ErrorCode.NOT_FOUND) {
err.suggestions = [
"Use list_audits to see your available audit reports.",
"Create a new audit with create_audit.",
];
}
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error ? err.message : "Failed to fetch audit",
);
return { text: error.toResponseText(), isError: true };
}
}
/**
* Execute list_audits tool.
*/
export async function executeListAudits(
input: ListAuditsInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("list_audits");
if (tierCheck) return tierCheck;
debug("Listing audits", { limit: input.limit, offset: input.offset });
try {
const response = await apiRequest<AuditListResponse>(
`/api/v1/audits?limit=${input.limit}&offset=${input.offset}`,
);
if (response.audits.length === 0) {
return {
text: `## Your Audits\n\nNo audit reports found. Create one with the \`create_audit\` tool.`,
};
}
let text = `## Your Audits (${response.audits.length} of ${response.total})\n\n`;
text += `| Name | Health Score | Status | Created |\n`;
text += `|------|-------------|--------|--------|\n`;
for (const audit of response.audits) {
const score = audit.summary?.healthScore ?? "-";
const date = new Date(audit.createdAt).toLocaleDateString();
text += `| ${audit.name} | ${score}/100 | ${audit.status} | ${date} |\n`;
}
if (response.total > response.audits.length) {
text += `\n_Showing ${response.audits.length} of ${response.total} audits. Use offset parameter for more._`;
}
return { text };
} catch (err) {
if (err instanceof McpError) {
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error ? err.message : "Failed to list audits",
);
return { text: error.toResponseText(), isError: true };
}
}
/**
* Execute compare_audits tool.
*/
export async function executeCompareAudits(
input: CompareAuditsInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("compare_audits");
if (tierCheck) return tierCheck;
debug("Comparing audits", {
base: input.baseAuditId,
compare: input.compareAuditId,
});
try {
const response = await apiRequest<CompareResponse>(
"/api/v1/audits/compare",
{
method: "POST",
body: {
baseAuditId: input.baseAuditId,
compareAuditId: input.compareAuditId,
},
},
);
const text = formatComparison(response.comparison);
return { text };
} catch (err) {
if (err instanceof McpError) {
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error ? err.message : "Failed to compare audits",
);
return { text: error.toResponseText(), isError: true };
}
}
/**
* Execute get_audit_quota tool.
*/
export async function executeGetAuditQuota(): Promise<{
text: string;
isError?: boolean;
}> {
// Check Pro access
const tierCheck = await checkProAccess("get_audit_quota");
if (tierCheck) return tierCheck;
debug("Getting audit quota");
try {
const response = await apiRequest<QuotaResponse>("/api/v1/audits/quota");
const { quota } = response;
let text = `## Audit Quota\n\n`;
text += `| Metric | Value |\n`;
text += `|--------|-------|\n`;
text += `| Used | ${quota.used} |\n`;
text += `| Limit | ${quota.limit} |\n`;
text += `| Remaining | ${quota.remaining} |\n`;
if (quota.resetsAt) {
text += `| Resets At | ${new Date(quota.resetsAt).toLocaleDateString()} |\n`;
}
return { text };
} catch (err) {
if (err instanceof McpError) {
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error ? err.message : "Failed to get audit quota",
);
return { text: error.toResponseText(), isError: true };
}
}
/**
* Execute get_migration_recommendation tool.
*/
export async function executeGetMigrationRecommendation(
input: GetMigrationRecommendationInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("get_migration_recommendation");
if (tierCheck) return tierCheck;
debug("Getting migration recommendation", { auditId: input.auditId });
try {
const response = await apiRequest<MigrationRecommendationResponse>(
`/api/v1/audits/${input.auditId}/migration`,
);
const text = formatMigrationRecommendation(response);
return { text };
} catch (err) {
if (err instanceof McpError) {
if (err.code === ErrorCode.NOT_FOUND) {
err.suggestions = [
"Use list_audits to see your available audit reports.",
"Make sure the audit is completed before requesting migration analysis.",
"Create a new audit with create_audit.",
];
}
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error
? err.message
: "Failed to get migration recommendation",
);
return { text: error.toResponseText(), isError: true };
}
}
// ============================================================================
// BETTER-T-STACK IMPORT
// ============================================================================
/**
* Input schema for import_better_t_stack tool.
*/
export const ImportBetterTStackInputSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("github"),
url: z.string().url().describe("GitHub repository URL"),
name: z.string().min(1).max(200).optional().describe("Custom audit name"),
}),
z.object({
type: z.literal("package-json"),
content: z.string().min(2).max(500000).describe("Raw package.json content"),
name: z.string().min(1).max(200).optional().describe("Custom audit name"),
}),
]);
export type ImportBetterTStackInput = z.infer<
typeof ImportBetterTStackInputSchema
>;
export const importBetterTStackToolDefinition = {
name: "import_better_t_stack",
description: `Import a Better-T-Stack project and create an audit.
**Use case**: If a project was scaffolded with Better-T-Stack CLI, you can import it directly for a technical debt audit.
**Input options**:
1. GitHub URL - We'll fetch the package.json automatically
2. Raw package.json content - Paste the file contents directly
**What happens**:
- Detects technologies from dependencies
- Calculates a "BTS confidence score" (higher = more likely a Better-T-Stack project)
- Creates and runs an audit automatically
- Returns findings and health score
**Tier**: Requires Pro or Team subscription (OR OAuth session)
**Example (GitHub)**:
\`\`\`json
{
"type": "github",
"url": "https://github.com/username/my-bts-app",
"name": "My BTS App Audit"
}
\`\`\`
**Example (package.json)**:
\`\`\`json
{
"type": "package-json",
"content": "{\"name\": \"my-app\", \"dependencies\": {...}}"
}
\`\`\`
**Next Steps after import**:
- Get migration plan: \`get_migration_recommendation({ auditId: "uuid" })\`
- Compare with future audits: \`compare_audits({ baseAuditId, compareAuditId })\``,
inputSchema: {
type: "object" as const,
oneOf: [
{
type: "object" as const,
properties: {
type: { type: "string", const: "github" },
url: { type: "string", description: "GitHub repository URL" },
name: { type: "string", description: "Custom audit name (optional)" },
},
required: ["type", "url"],
},
{
type: "object" as const,
properties: {
type: { type: "string", const: "package-json" },
content: { type: "string", description: "Raw package.json content" },
name: { type: "string", description: "Custom audit name (optional)" },
},
required: ["type", "content"],
},
],
},
};
interface ImportBetterTStackResponse {
audit: AuditResponse;
import: {
source: "github" | "package-json";
projectName: string;
betterTStackVersion?: string;
confidence: number;
technologiesDetected: number;
techSummary: Record<string, string[]>;
warnings: string[];
};
}
function formatImportResult(response: ImportBetterTStackResponse): string {
const { audit, import: imp } = response;
let text = `## Better-T-Stack Import Successful\n\n`;
// Import metadata
text += `### Project Analysis\n`;
text += `| Metric | Value |\n`;
text += `|--------|-------|\n`;
text += `| Project Name | ${imp.projectName} |\n`;
text += `| Import Source | ${imp.source} |\n`;
text += `| BTS Confidence | ${imp.confidence}% |\n`;
text += `| Technologies Detected | ${imp.technologiesDetected} |\n`;
if (imp.betterTStackVersion) {
text += `| Better-T-Stack Version | ${imp.betterTStackVersion} |\n`;
}
text += `\n`;
// Tech summary by category
const nonEmptyCategories = Object.entries(imp.techSummary).filter(
([, techs]) => techs.length > 0,
);
if (nonEmptyCategories.length > 0) {
text += `### Detected Stack\n`;
for (const [category, techs] of nonEmptyCategories) {
text += `- **${category}**: ${techs.join(", ")}\n`;
}
text += `\n`;
}
// Warnings
if (imp.warnings.length > 0) {
text += `### Warnings\n`;
for (const warning of imp.warnings) {
text += `- [WARN] ${warning}\n`;
}
text += `\n`;
}
// Audit results
text += `---\n\n`;
text += formatAuditReport(audit);
return text;
}
/**
* Execute import_better_t_stack tool.
*/
export async function executeImportBetterTStack(
input: ImportBetterTStackInput,
): Promise<{ text: string; isError?: boolean }> {
// Check Pro access
const tierCheck = await checkProAccess("import_better_t_stack");
if (tierCheck) return tierCheck;
debug("Importing Better-T-Stack project", {
type: input.type,
...(input.type === "github"
? { url: input.url }
: { contentLength: input.content.length }),
});
try {
const response = await apiRequest<ImportBetterTStackResponse>(
"/api/v1/audits/import/better-t-stack",
{
method: "POST",
body: input,
timeoutMs: 30000,
},
);
const text = formatImportResult(response);
return { text };
} catch (err) {
if (err instanceof McpError) {
if (err.code === ErrorCode.INVALID_INPUT) {
err.suggestions = [
"For GitHub: Use format https://github.com/owner/repo",
"For package.json: Make sure the content is valid JSON",
"Check that the package.json has dependencies defined",
];
}
return { text: err.toResponseText(), isError: true };
}
const error = new McpError(
ErrorCode.API_ERROR,
err instanceof Error
? err.message
: "Failed to import Better-T-Stack project",
);
return { text: error.toResponseText(), isError: true };
}
}