@stacksfinder/mcp-server 1.5.1 → 1.6.0

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.
@@ -1,7 +1,7 @@
1
- import { z } from 'zod';
2
- import { apiRequest } from '../utils/api-client.js';
3
- import { McpError, ErrorCode, checkProAccess } from '../utils/errors.js';
4
- import { debug } from '../utils/logger.js';
1
+ import { z } from "zod";
2
+ import { apiRequest } from "../utils/api-client.js";
3
+ import { McpError, ErrorCode, checkProAccess } from "../utils/errors.js";
4
+ import { debug } from "../utils/logger.js";
5
5
  // ============================================================================
6
6
  // SCHEMAS
7
7
  // ============================================================================
@@ -9,36 +9,51 @@ import { debug } from '../utils/logger.js';
9
9
  * Input schema for create_audit tool.
10
10
  */
11
11
  export const CreateAuditInputSchema = z.object({
12
- name: z.string().min(1).max(200).describe('Name for the audit report'),
12
+ name: z.string().min(1).max(200).describe("Name for the audit report"),
13
13
  technologies: z
14
14
  .array(z.object({
15
- name: z.string().min(1).describe('Technology name (e.g., "react", "express", "lodash")'),
16
- version: z.string().optional().describe('Version string (e.g., "18.2.0", "4.17.21")'),
17
- category: z.string().optional().describe('Optional category (e.g., "frontend", "backend")')
15
+ name: z
16
+ .string()
17
+ .min(1)
18
+ .describe('Technology name (e.g., "react", "express", "lodash")'),
19
+ version: z
20
+ .string()
21
+ .optional()
22
+ .describe('Version string (e.g., "18.2.0", "4.17.21")'),
23
+ category: z
24
+ .string()
25
+ .optional()
26
+ .describe('Optional category (e.g., "frontend", "backend")'),
18
27
  }))
19
28
  .min(1)
20
29
  .max(50)
21
- .describe('List of technologies to audit')
30
+ .describe("List of technologies to audit"),
22
31
  });
23
32
  /**
24
33
  * Input schema for get_audit tool.
25
34
  */
26
35
  export const GetAuditInputSchema = z.object({
27
- auditId: z.string().uuid().describe('Audit report UUID')
36
+ auditId: z.string().uuid().describe("Audit report UUID"),
28
37
  });
29
38
  /**
30
39
  * Input schema for list_audits tool.
31
40
  */
32
41
  export const ListAuditsInputSchema = z.object({
33
- limit: z.number().min(1).max(50).optional().default(10).describe('Max results to return'),
34
- offset: z.number().min(0).optional().default(0).describe('Pagination offset')
42
+ limit: z
43
+ .number()
44
+ .min(1)
45
+ .max(50)
46
+ .optional()
47
+ .default(10)
48
+ .describe("Max results to return"),
49
+ offset: z.number().min(0).optional().default(0).describe("Pagination offset"),
35
50
  });
36
51
  /**
37
52
  * Input schema for compare_audits tool.
38
53
  */
39
54
  export const CompareAuditsInputSchema = z.object({
40
- baseAuditId: z.string().uuid().describe('Base audit ID (older)'),
41
- compareAuditId: z.string().uuid().describe('Compare audit ID (newer)')
55
+ baseAuditId: z.string().uuid().describe("Base audit ID (older)"),
56
+ compareAuditId: z.string().uuid().describe("Compare audit ID (newer)"),
42
57
  });
43
58
  /**
44
59
  * Input schema for get_audit_quota tool.
@@ -48,165 +63,168 @@ export const GetAuditQuotaInputSchema = z.object({});
48
63
  * Input schema for get_migration_recommendation tool.
49
64
  */
50
65
  export const GetMigrationRecommendationInputSchema = z.object({
51
- auditId: z.string().uuid().describe('Audit report UUID to analyze for migration')
66
+ auditId: z
67
+ .string()
68
+ .uuid()
69
+ .describe("Audit report UUID to analyze for migration"),
52
70
  });
53
71
  // ============================================================================
54
72
  // TOOL DEFINITIONS
55
73
  // ============================================================================
56
74
  export const createAuditToolDefinition = {
57
- name: 'create_audit',
58
- description: `Create a technical debt audit for your tech stack. Analyzes for deprecated packages, security vulnerabilities, EOL versions, and upgrade recommendations.
59
-
60
- **Tier**: Requires Pro or Team subscription (OR OAuth session)
61
-
62
- **Prerequisites**:
63
- - Pro/Team account or authenticated via OAuth
64
- - List of technologies with versions (use package.json data)
65
-
66
- **Next Steps**:
67
- - Get full report: \`get_audit({ auditId: "returned-uuid" })\`
68
- - Get migration plan: \`get_migration_recommendation({ auditId: "uuid" })\`
69
- - Compare over time: \`compare_audits({ baseAuditId, compareAuditId })\`
70
-
71
- **Output includes**:
72
- - Health score (0-100)
73
- - Findings by severity (critical/high/medium/low/info)
74
- - Actionable upgrade recommendations
75
- - CVE detection for known vulnerabilities
76
-
77
- **Common Pitfalls**:
78
- - Include version numbers for accurate vulnerability detection
79
- - Use technology names as they appear in package managers
80
-
75
+ name: "create_audit",
76
+ description: `Create a technical debt audit for your tech stack. Analyzes for deprecated packages, security vulnerabilities, EOL versions, and upgrade recommendations.
77
+
78
+ **Tier**: Requires Pro or Team subscription (OR OAuth session)
79
+
80
+ **Prerequisites**:
81
+ - Pro/Team account or authenticated via OAuth
82
+ - List of technologies with versions (use package.json data)
83
+
84
+ **Next Steps**:
85
+ - Get full report: \`get_audit({ auditId: "returned-uuid" })\`
86
+ - Get migration plan: \`get_migration_recommendation({ auditId: "uuid" })\`
87
+ - Compare over time: \`compare_audits({ baseAuditId, compareAuditId })\`
88
+
89
+ **Output includes**:
90
+ - Health score (0-100)
91
+ - Findings by severity (critical/high/medium/low/info)
92
+ - Actionable upgrade recommendations
93
+ - CVE detection for known vulnerabilities
94
+
95
+ **Common Pitfalls**:
96
+ - Include version numbers for accurate vulnerability detection
97
+ - Use technology names as they appear in package managers
98
+
81
99
  **Example**: \`create_audit({ name: "Q1 2026 Stack Review", technologies: [{ name: "React", version: "17.0.0" }, { name: "Node.js", version: "14.0.0" }] })\``,
82
100
  inputSchema: {
83
- type: 'object',
101
+ type: "object",
84
102
  properties: {
85
103
  name: {
86
- type: 'string',
87
- description: 'Name for the audit report (e.g., "Q1 2026 Stack Review")'
104
+ type: "string",
105
+ description: 'Name for the audit report (e.g., "Q1 2026 Stack Review")',
88
106
  },
89
107
  technologies: {
90
- type: 'array',
108
+ type: "array",
91
109
  items: {
92
- type: 'object',
110
+ type: "object",
93
111
  properties: {
94
- name: { type: 'string', description: 'Technology name' },
95
- version: { type: 'string', description: 'Version (optional)' },
96
- category: { type: 'string', description: 'Category (optional)' }
112
+ name: { type: "string", description: "Technology name" },
113
+ version: { type: "string", description: "Version (optional)" },
114
+ category: { type: "string", description: "Category (optional)" },
97
115
  },
98
- required: ['name']
116
+ required: ["name"],
99
117
  },
100
- description: 'Technologies to audit'
101
- }
118
+ description: "Technologies to audit",
119
+ },
102
120
  },
103
- required: ['name', 'technologies']
104
- }
121
+ required: ["name", "technologies"],
122
+ },
105
123
  };
106
124
  export const getAuditToolDefinition = {
107
- name: 'get_audit',
108
- description: 'Fetch a completed audit report by ID. Returns all findings and health score.',
125
+ name: "get_audit",
126
+ description: "Fetch a completed audit report by ID. Returns all findings and health score.",
109
127
  inputSchema: {
110
- type: 'object',
128
+ type: "object",
111
129
  properties: {
112
130
  auditId: {
113
- type: 'string',
114
- format: 'uuid',
115
- description: 'Audit report UUID'
116
- }
131
+ type: "string",
132
+ format: "uuid",
133
+ description: "Audit report UUID",
134
+ },
117
135
  },
118
- required: ['auditId']
119
- }
136
+ required: ["auditId"],
137
+ },
120
138
  };
121
139
  export const listAuditsToolDefinition = {
122
- name: 'list_audits',
123
- description: 'List your audit reports with pagination. Shows name, status, health score, and creation date.',
140
+ name: "list_audits",
141
+ description: "List your audit reports with pagination. Shows name, status, health score, and creation date.",
124
142
  inputSchema: {
125
- type: 'object',
143
+ type: "object",
126
144
  properties: {
127
145
  limit: {
128
- type: 'number',
129
- description: 'Max results (1-50, default 10)'
146
+ type: "number",
147
+ description: "Max results (1-50, default 10)",
130
148
  },
131
149
  offset: {
132
- type: 'number',
133
- description: 'Pagination offset (default 0)'
134
- }
150
+ type: "number",
151
+ description: "Pagination offset (default 0)",
152
+ },
135
153
  },
136
- required: []
137
- }
154
+ required: [],
155
+ },
138
156
  };
139
157
  export const compareAuditsToolDefinition = {
140
- name: 'compare_audits',
141
- description: `Compare two audit reports to track technical debt trends over time.
142
- Shows new issues introduced, issues resolved, and health score change.
158
+ name: "compare_audits",
159
+ description: `Compare two audit reports to track technical debt trends over time.
160
+ Shows new issues introduced, issues resolved, and health score change.
143
161
  Useful for measuring progress on debt reduction.`,
144
162
  inputSchema: {
145
- type: 'object',
163
+ type: "object",
146
164
  properties: {
147
165
  baseAuditId: {
148
- type: 'string',
149
- format: 'uuid',
150
- description: 'Base (older) audit ID'
166
+ type: "string",
167
+ format: "uuid",
168
+ description: "Base (older) audit ID",
151
169
  },
152
170
  compareAuditId: {
153
- type: 'string',
154
- format: 'uuid',
155
- description: 'Compare (newer) audit ID'
156
- }
171
+ type: "string",
172
+ format: "uuid",
173
+ description: "Compare (newer) audit ID",
174
+ },
157
175
  },
158
- required: ['baseAuditId', 'compareAuditId']
159
- }
176
+ required: ["baseAuditId", "compareAuditId"],
177
+ },
160
178
  };
161
179
  export const getAuditQuotaToolDefinition = {
162
- name: 'get_audit_quota',
163
- description: 'Check your remaining audit quota for this month.',
180
+ name: "get_audit_quota",
181
+ description: "Check your remaining audit quota for this month.",
164
182
  inputSchema: {
165
- type: 'object',
183
+ type: "object",
166
184
  properties: {},
167
- required: []
168
- }
185
+ required: [],
186
+ },
169
187
  };
170
188
  export const getMigrationRecommendationToolDefinition = {
171
- name: 'get_migration_recommendation',
172
- description: `Analyze an audit report for migration opportunities.
173
- Returns a detailed migration recommendation including:
174
- - Technologies that should be replaced
175
- - Recommended modern alternatives
176
- - Migration roadmap with phases
177
- - Risk assessment
178
- - Builder constraints to pre-fill for generating a migration blueprint
179
-
189
+ name: "get_migration_recommendation",
190
+ description: `Analyze an audit report for migration opportunities.
191
+ Returns a detailed migration recommendation including:
192
+ - Technologies that should be replaced
193
+ - Recommended modern alternatives
194
+ - Migration roadmap with phases
195
+ - Risk assessment
196
+ - Builder constraints to pre-fill for generating a migration blueprint
197
+
180
198
  Use this after create_audit to get actionable migration guidance.`,
181
199
  inputSchema: {
182
- type: 'object',
200
+ type: "object",
183
201
  properties: {
184
202
  auditId: {
185
- type: 'string',
186
- format: 'uuid',
187
- description: 'Audit report UUID to analyze'
188
- }
203
+ type: "string",
204
+ format: "uuid",
205
+ description: "Audit report UUID to analyze",
206
+ },
189
207
  },
190
- required: ['auditId']
191
- }
208
+ required: ["auditId"],
209
+ },
192
210
  };
193
211
  // ============================================================================
194
212
  // FORMATTERS
195
213
  // ============================================================================
196
214
  function formatSeverityMarker(severity) {
197
215
  switch (severity) {
198
- case 'critical':
199
- return '[CRITICAL]';
200
- case 'high':
201
- return '[HIGH]';
202
- case 'medium':
203
- return '[MEDIUM]';
204
- case 'low':
205
- return '[LOW]';
206
- case 'info':
207
- return '[INFO]';
216
+ case "critical":
217
+ return "[CRITICAL]";
218
+ case "high":
219
+ return "[HIGH]";
220
+ case "medium":
221
+ return "[MEDIUM]";
222
+ case "low":
223
+ return "[LOW]";
224
+ case "info":
225
+ return "[INFO]";
208
226
  default:
209
- return '[-]';
227
+ return "[-]";
210
228
  }
211
229
  }
212
230
  function formatHealthScore(score) {
@@ -255,7 +273,7 @@ function formatAuditReport(audit) {
255
273
  text += `**Migration Effort**: ${finding.migrationEffort}\n`;
256
274
  }
257
275
  if (finding.cveIds && finding.cveIds.length > 0) {
258
- text += `**CVEs**: ${finding.cveIds.join(', ')}\n`;
276
+ text += `**CVEs**: ${finding.cveIds.join(", ")}\n`;
259
277
  }
260
278
  text += `\n**Action**: ${finding.suggestedAction}\n\n`;
261
279
  if (finding.references && finding.references.length > 0) {
@@ -267,7 +285,7 @@ function formatAuditReport(audit) {
267
285
  text += `---\n\n`;
268
286
  }
269
287
  }
270
- else if (audit.status === 'completed') {
288
+ else if (audit.status === "completed") {
271
289
  text += `\n### All Clear\n`;
272
290
  text += `Your stack passed all checks. Great job maintaining your technical health!\n`;
273
291
  }
@@ -281,13 +299,13 @@ function formatComparison(comparison) {
281
299
  text += `| ${comparison.baseAudit.name} (base) | ${comparison.baseAudit.healthScore}/100 |\n`;
282
300
  text += `| ${comparison.compareAudit.name} (compare) | ${comparison.compareAudit.healthScore}/100 |\n`;
283
301
  text += `\n`;
284
- const trendMarker = comparison.trend === 'improving'
285
- ? '[UP]'
286
- : comparison.trend === 'degrading'
287
- ? '[DOWN]'
288
- : '[STABLE]';
302
+ const trendMarker = comparison.trend === "improving"
303
+ ? "[UP]"
304
+ : comparison.trend === "degrading"
305
+ ? "[DOWN]"
306
+ : "[STABLE]";
289
307
  text += `### Trend: ${trendMarker} ${comparison.trend.toUpperCase()}\n`;
290
- text += `**Health Score Change**: ${comparison.healthScoreDelta > 0 ? '+' : ''}${comparison.healthScoreDelta} points\n\n`;
308
+ text += `**Health Score Change**: ${comparison.healthScoreDelta > 0 ? "+" : ""}${comparison.healthScoreDelta} points\n\n`;
291
309
  if (comparison.resolvedCount > 0) {
292
310
  text += `### Resolved Issues (${comparison.resolvedCount})\n`;
293
311
  for (const finding of comparison.resolvedFindings) {
@@ -317,10 +335,10 @@ function formatMigrationRecommendation(response) {
317
335
  return `## Migration Analysis\n\nUnable to generate recommendation.`;
318
336
  }
319
337
  const urgencyMarker = {
320
- critical: '[CRITICAL]',
321
- high: '[HIGH]',
322
- medium: '[MEDIUM]',
323
- low: '[LOW]'
338
+ critical: "[CRITICAL]",
339
+ high: "[HIGH]",
340
+ medium: "[MEDIUM]",
341
+ low: "[LOW]",
324
342
  }[rec.urgency];
325
343
  let text = `## ${urgencyMarker} Migration Recommendation\n\n`;
326
344
  text += `### ${rec.title}\n\n`;
@@ -337,7 +355,7 @@ function formatMigrationRecommendation(response) {
337
355
  text += `| Technology | Version | Reason |\n`;
338
356
  text += `|------------|---------|--------|\n`;
339
357
  for (const tech of rec.techsToReplace) {
340
- text += `| ${tech.name} | ${tech.currentVersion || '-'} | ${tech.reason} |\n`;
358
+ text += `| ${tech.name} | ${tech.currentVersion || "-"} | ${tech.reason} |\n`;
341
359
  }
342
360
  text += `\n`;
343
361
  }
@@ -346,8 +364,8 @@ function formatMigrationRecommendation(response) {
346
364
  text += `### Recommended Alternatives\n\n`;
347
365
  for (const alt of rec.suggestedAlternatives) {
348
366
  text += `**${alt.forTech}** → `;
349
- const alts = alt.alternatives.map(a => a === alt.preferredChoice ? `**${a}** (recommended)` : a);
350
- text += alts.join(', ');
367
+ const alts = alt.alternatives.map((a) => a === alt.preferredChoice ? `**${a}** (recommended)` : a);
368
+ text += alts.join(", ");
351
369
  text += `\n`;
352
370
  text += ` _${alt.reason}_\n\n`;
353
371
  }
@@ -359,7 +377,7 @@ function formatMigrationRecommendation(response) {
359
377
  text += `**Phase ${step.order}: ${step.phase}** (${step.effort} effort)\n`;
360
378
  text += `${step.description}\n`;
361
379
  if (step.techsAffected.length > 0) {
362
- text += `Affects: ${step.techsAffected.join(', ')}\n`;
380
+ text += `Affects: ${step.techsAffected.join(", ")}\n`;
363
381
  }
364
382
  text += `\n`;
365
383
  }
@@ -368,7 +386,11 @@ function formatMigrationRecommendation(response) {
368
386
  if (rec.risks.length > 0) {
369
387
  text += `### Risk Assessment\n\n`;
370
388
  for (const risk of rec.risks) {
371
- const riskMarker = risk.level === 'high' ? '[HIGH]' : risk.level === 'medium' ? '[MEDIUM]' : '[LOW]';
389
+ const riskMarker = risk.level === "high"
390
+ ? "[HIGH]"
391
+ : risk.level === "medium"
392
+ ? "[MEDIUM]"
393
+ : "[LOW]";
372
394
  text += `${riskMarker} **${risk.level.toUpperCase()} RISK**: ${risk.description}\n`;
373
395
  text += ` _Mitigation_: ${risk.mitigation}\n\n`;
374
396
  }
@@ -377,7 +399,7 @@ function formatMigrationRecommendation(response) {
377
399
  if (rec.inferredConstraints.length > 0) {
378
400
  text += `### Inferred Builder Constraints\n\n`;
379
401
  text += `These constraints will be pre-filled when generating a migration blueprint:\n`;
380
- text += rec.inferredConstraints.map(c => `\`${c}\``).join(', ');
402
+ text += rec.inferredConstraints.map((c) => `\`${c}\``).join(", ");
381
403
  text += `\n\n`;
382
404
  }
383
405
  // Builder Pre-fill info
@@ -391,10 +413,10 @@ function formatMigrationRecommendation(response) {
391
413
  text += `- **Scale**: ${response.builderPreFill.context.scale}\n`;
392
414
  }
393
415
  if (response.builderPreFill.context.priorities.length > 0) {
394
- text += `- **Priorities**: ${response.builderPreFill.context.priorities.join(', ')}\n`;
416
+ text += `- **Priorities**: ${response.builderPreFill.context.priorities.join(", ")}\n`;
395
417
  }
396
418
  if (response.builderPreFill.hints.techsToAvoid.length > 0) {
397
- text += `- **Avoid**: ${response.builderPreFill.hints.techsToAvoid.join(', ')}\n`;
419
+ text += `- **Avoid**: ${response.builderPreFill.hints.techsToAvoid.join(", ")}\n`;
398
420
  }
399
421
  text += `\nVisit https://stacksfinder.com/builder to generate your migration blueprint.\n`;
400
422
  }
@@ -408,21 +430,24 @@ function formatMigrationRecommendation(response) {
408
430
  */
409
431
  export async function executeCreateAudit(input) {
410
432
  // Check Pro access
411
- const tierCheck = await checkProAccess('create_audit');
433
+ const tierCheck = await checkProAccess("create_audit");
412
434
  if (tierCheck)
413
435
  return tierCheck;
414
- debug('Creating audit', { name: input.name, techCount: input.technologies.length });
436
+ debug("Creating audit", {
437
+ name: input.name,
438
+ techCount: input.technologies.length,
439
+ });
415
440
  try {
416
- const response = await apiRequest('/api/v1/audits', {
417
- method: 'POST',
441
+ const response = await apiRequest("/api/v1/audits", {
442
+ method: "POST",
418
443
  body: {
419
444
  name: input.name,
420
445
  stackInput: {
421
- technologies: input.technologies
446
+ technologies: input.technologies,
422
447
  },
423
- source: 'mcp'
448
+ source: "mcp",
424
449
  },
425
- timeoutMs: 30000
450
+ timeoutMs: 30000,
426
451
  });
427
452
  const text = formatAuditReport(response);
428
453
  return { text };
@@ -431,7 +456,7 @@ export async function executeCreateAudit(input) {
431
456
  if (err instanceof McpError) {
432
457
  return { text: err.toResponseText(), isError: true };
433
458
  }
434
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to create audit');
459
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : "Failed to create audit");
435
460
  return { text: error.toResponseText(), isError: true };
436
461
  }
437
462
  }
@@ -440,10 +465,10 @@ export async function executeCreateAudit(input) {
440
465
  */
441
466
  export async function executeGetAudit(input) {
442
467
  // Check Pro access
443
- const tierCheck = await checkProAccess('get_audit');
468
+ const tierCheck = await checkProAccess("get_audit");
444
469
  if (tierCheck)
445
470
  return tierCheck;
446
- debug('Fetching audit', { auditId: input.auditId });
471
+ debug("Fetching audit", { auditId: input.auditId });
447
472
  try {
448
473
  const response = await apiRequest(`/api/v1/audits/${input.auditId}`);
449
474
  const text = formatAuditReport(response);
@@ -453,13 +478,13 @@ export async function executeGetAudit(input) {
453
478
  if (err instanceof McpError) {
454
479
  if (err.code === ErrorCode.NOT_FOUND) {
455
480
  err.suggestions = [
456
- 'Use list_audits to see your available audit reports.',
457
- 'Create a new audit with create_audit.'
481
+ "Use list_audits to see your available audit reports.",
482
+ "Create a new audit with create_audit.",
458
483
  ];
459
484
  }
460
485
  return { text: err.toResponseText(), isError: true };
461
486
  }
462
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to fetch audit');
487
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : "Failed to fetch audit");
463
488
  return { text: error.toResponseText(), isError: true };
464
489
  }
465
490
  }
@@ -468,22 +493,22 @@ export async function executeGetAudit(input) {
468
493
  */
469
494
  export async function executeListAudits(input) {
470
495
  // Check Pro access
471
- const tierCheck = await checkProAccess('list_audits');
496
+ const tierCheck = await checkProAccess("list_audits");
472
497
  if (tierCheck)
473
498
  return tierCheck;
474
- debug('Listing audits', { limit: input.limit, offset: input.offset });
499
+ debug("Listing audits", { limit: input.limit, offset: input.offset });
475
500
  try {
476
501
  const response = await apiRequest(`/api/v1/audits?limit=${input.limit}&offset=${input.offset}`);
477
502
  if (response.audits.length === 0) {
478
503
  return {
479
- text: `## Your Audits\n\nNo audit reports found. Create one with the \`create_audit\` tool.`
504
+ text: `## Your Audits\n\nNo audit reports found. Create one with the \`create_audit\` tool.`,
480
505
  };
481
506
  }
482
507
  let text = `## Your Audits (${response.audits.length} of ${response.total})\n\n`;
483
508
  text += `| Name | Health Score | Status | Created |\n`;
484
509
  text += `|------|-------------|--------|--------|\n`;
485
510
  for (const audit of response.audits) {
486
- const score = audit.summary?.healthScore ?? '-';
511
+ const score = audit.summary?.healthScore ?? "-";
487
512
  const date = new Date(audit.createdAt).toLocaleDateString();
488
513
  text += `| ${audit.name} | ${score}/100 | ${audit.status} | ${date} |\n`;
489
514
  }
@@ -496,7 +521,7 @@ export async function executeListAudits(input) {
496
521
  if (err instanceof McpError) {
497
522
  return { text: err.toResponseText(), isError: true };
498
523
  }
499
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to list audits');
524
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : "Failed to list audits");
500
525
  return { text: error.toResponseText(), isError: true };
501
526
  }
502
527
  }
@@ -505,17 +530,20 @@ export async function executeListAudits(input) {
505
530
  */
506
531
  export async function executeCompareAudits(input) {
507
532
  // Check Pro access
508
- const tierCheck = await checkProAccess('compare_audits');
533
+ const tierCheck = await checkProAccess("compare_audits");
509
534
  if (tierCheck)
510
535
  return tierCheck;
511
- debug('Comparing audits', { base: input.baseAuditId, compare: input.compareAuditId });
536
+ debug("Comparing audits", {
537
+ base: input.baseAuditId,
538
+ compare: input.compareAuditId,
539
+ });
512
540
  try {
513
- const response = await apiRequest('/api/v1/audits/compare', {
514
- method: 'POST',
541
+ const response = await apiRequest("/api/v1/audits/compare", {
542
+ method: "POST",
515
543
  body: {
516
544
  baseAuditId: input.baseAuditId,
517
- compareAuditId: input.compareAuditId
518
- }
545
+ compareAuditId: input.compareAuditId,
546
+ },
519
547
  });
520
548
  const text = formatComparison(response.comparison);
521
549
  return { text };
@@ -524,7 +552,7 @@ export async function executeCompareAudits(input) {
524
552
  if (err instanceof McpError) {
525
553
  return { text: err.toResponseText(), isError: true };
526
554
  }
527
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to compare audits');
555
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : "Failed to compare audits");
528
556
  return { text: error.toResponseText(), isError: true };
529
557
  }
530
558
  }
@@ -533,12 +561,12 @@ export async function executeCompareAudits(input) {
533
561
  */
534
562
  export async function executeGetAuditQuota() {
535
563
  // Check Pro access
536
- const tierCheck = await checkProAccess('get_audit_quota');
564
+ const tierCheck = await checkProAccess("get_audit_quota");
537
565
  if (tierCheck)
538
566
  return tierCheck;
539
- debug('Getting audit quota');
567
+ debug("Getting audit quota");
540
568
  try {
541
- const response = await apiRequest('/api/v1/audits/quota');
569
+ const response = await apiRequest("/api/v1/audits/quota");
542
570
  const { quota } = response;
543
571
  let text = `## Audit Quota\n\n`;
544
572
  text += `| Metric | Value |\n`;
@@ -555,7 +583,7 @@ export async function executeGetAuditQuota() {
555
583
  if (err instanceof McpError) {
556
584
  return { text: err.toResponseText(), isError: true };
557
585
  }
558
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to get audit quota');
586
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : "Failed to get audit quota");
559
587
  return { text: error.toResponseText(), isError: true };
560
588
  }
561
589
  }
@@ -564,10 +592,10 @@ export async function executeGetAuditQuota() {
564
592
  */
565
593
  export async function executeGetMigrationRecommendation(input) {
566
594
  // Check Pro access
567
- const tierCheck = await checkProAccess('get_migration_recommendation');
595
+ const tierCheck = await checkProAccess("get_migration_recommendation");
568
596
  if (tierCheck)
569
597
  return tierCheck;
570
- debug('Getting migration recommendation', { auditId: input.auditId });
598
+ debug("Getting migration recommendation", { auditId: input.auditId });
571
599
  try {
572
600
  const response = await apiRequest(`/api/v1/audits/${input.auditId}/migration`);
573
601
  const text = formatMigrationRecommendation(response);
@@ -577,14 +605,173 @@ export async function executeGetMigrationRecommendation(input) {
577
605
  if (err instanceof McpError) {
578
606
  if (err.code === ErrorCode.NOT_FOUND) {
579
607
  err.suggestions = [
580
- 'Use list_audits to see your available audit reports.',
581
- 'Make sure the audit is completed before requesting migration analysis.',
582
- 'Create a new audit with create_audit.'
608
+ "Use list_audits to see your available audit reports.",
609
+ "Make sure the audit is completed before requesting migration analysis.",
610
+ "Create a new audit with create_audit.",
611
+ ];
612
+ }
613
+ return { text: err.toResponseText(), isError: true };
614
+ }
615
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error
616
+ ? err.message
617
+ : "Failed to get migration recommendation");
618
+ return { text: error.toResponseText(), isError: true };
619
+ }
620
+ }
621
+ // ============================================================================
622
+ // BETTER-T-STACK IMPORT
623
+ // ============================================================================
624
+ /**
625
+ * Input schema for import_better_t_stack tool.
626
+ */
627
+ export const ImportBetterTStackInputSchema = z.discriminatedUnion("type", [
628
+ z.object({
629
+ type: z.literal("github"),
630
+ url: z.string().url().describe("GitHub repository URL"),
631
+ name: z.string().min(1).max(200).optional().describe("Custom audit name"),
632
+ }),
633
+ z.object({
634
+ type: z.literal("package-json"),
635
+ content: z.string().min(2).max(500000).describe("Raw package.json content"),
636
+ name: z.string().min(1).max(200).optional().describe("Custom audit name"),
637
+ }),
638
+ ]);
639
+ export const importBetterTStackToolDefinition = {
640
+ name: "import_better_t_stack",
641
+ description: `Import a Better-T-Stack project and create an audit.
642
+
643
+ **Use case**: If a project was scaffolded with Better-T-Stack CLI, you can import it directly for a technical debt audit.
644
+
645
+ **Input options**:
646
+ 1. GitHub URL - We'll fetch the package.json automatically
647
+ 2. Raw package.json content - Paste the file contents directly
648
+
649
+ **What happens**:
650
+ - Detects technologies from dependencies
651
+ - Calculates a "BTS confidence score" (higher = more likely a Better-T-Stack project)
652
+ - Creates and runs an audit automatically
653
+ - Returns findings and health score
654
+
655
+ **Tier**: Requires Pro or Team subscription (OR OAuth session)
656
+
657
+ **Example (GitHub)**:
658
+ \`\`\`json
659
+ {
660
+ "type": "github",
661
+ "url": "https://github.com/username/my-bts-app",
662
+ "name": "My BTS App Audit"
663
+ }
664
+ \`\`\`
665
+
666
+ **Example (package.json)**:
667
+ \`\`\`json
668
+ {
669
+ "type": "package-json",
670
+ "content": "{\"name\": \"my-app\", \"dependencies\": {...}}"
671
+ }
672
+ \`\`\`
673
+
674
+ **Next Steps after import**:
675
+ - Get migration plan: \`get_migration_recommendation({ auditId: "uuid" })\`
676
+ - Compare with future audits: \`compare_audits({ baseAuditId, compareAuditId })\``,
677
+ inputSchema: {
678
+ type: "object",
679
+ oneOf: [
680
+ {
681
+ type: "object",
682
+ properties: {
683
+ type: { type: "string", const: "github" },
684
+ url: { type: "string", description: "GitHub repository URL" },
685
+ name: { type: "string", description: "Custom audit name (optional)" },
686
+ },
687
+ required: ["type", "url"],
688
+ },
689
+ {
690
+ type: "object",
691
+ properties: {
692
+ type: { type: "string", const: "package-json" },
693
+ content: { type: "string", description: "Raw package.json content" },
694
+ name: { type: "string", description: "Custom audit name (optional)" },
695
+ },
696
+ required: ["type", "content"],
697
+ },
698
+ ],
699
+ },
700
+ };
701
+ function formatImportResult(response) {
702
+ const { audit, import: imp } = response;
703
+ let text = `## Better-T-Stack Import Successful\n\n`;
704
+ // Import metadata
705
+ text += `### Project Analysis\n`;
706
+ text += `| Metric | Value |\n`;
707
+ text += `|--------|-------|\n`;
708
+ text += `| Project Name | ${imp.projectName} |\n`;
709
+ text += `| Import Source | ${imp.source} |\n`;
710
+ text += `| BTS Confidence | ${imp.confidence}% |\n`;
711
+ text += `| Technologies Detected | ${imp.technologiesDetected} |\n`;
712
+ if (imp.betterTStackVersion) {
713
+ text += `| Better-T-Stack Version | ${imp.betterTStackVersion} |\n`;
714
+ }
715
+ text += `\n`;
716
+ // Tech summary by category
717
+ const nonEmptyCategories = Object.entries(imp.techSummary).filter(([, techs]) => techs.length > 0);
718
+ if (nonEmptyCategories.length > 0) {
719
+ text += `### Detected Stack\n`;
720
+ for (const [category, techs] of nonEmptyCategories) {
721
+ text += `- **${category}**: ${techs.join(", ")}\n`;
722
+ }
723
+ text += `\n`;
724
+ }
725
+ // Warnings
726
+ if (imp.warnings.length > 0) {
727
+ text += `### Warnings\n`;
728
+ for (const warning of imp.warnings) {
729
+ text += `- [WARN] ${warning}\n`;
730
+ }
731
+ text += `\n`;
732
+ }
733
+ // Audit results
734
+ text += `---\n\n`;
735
+ text += formatAuditReport(audit);
736
+ return text;
737
+ }
738
+ /**
739
+ * Execute import_better_t_stack tool.
740
+ */
741
+ export async function executeImportBetterTStack(input) {
742
+ // Check Pro access
743
+ const tierCheck = await checkProAccess("import_better_t_stack");
744
+ if (tierCheck)
745
+ return tierCheck;
746
+ debug("Importing Better-T-Stack project", {
747
+ type: input.type,
748
+ ...(input.type === "github"
749
+ ? { url: input.url }
750
+ : { contentLength: input.content.length }),
751
+ });
752
+ try {
753
+ const response = await apiRequest("/api/v1/audits/import/better-t-stack", {
754
+ method: "POST",
755
+ body: input,
756
+ timeoutMs: 30000,
757
+ });
758
+ const text = formatImportResult(response);
759
+ return { text };
760
+ }
761
+ catch (err) {
762
+ if (err instanceof McpError) {
763
+ if (err.code === ErrorCode.INVALID_INPUT) {
764
+ err.suggestions = [
765
+ "For GitHub: Use format https://github.com/owner/repo",
766
+ "For package.json: Make sure the content is valid JSON",
767
+ "Check that the package.json has dependencies defined",
583
768
  ];
584
769
  }
585
770
  return { text: err.toResponseText(), isError: true };
586
771
  }
587
- const error = new McpError(ErrorCode.API_ERROR, err instanceof Error ? err.message : 'Failed to get migration recommendation');
772
+ const error = new McpError(ErrorCode.API_ERROR, err instanceof Error
773
+ ? err.message
774
+ : "Failed to import Better-T-Stack project");
588
775
  return { text: error.toResponseText(), isError: true };
589
776
  }
590
777
  }