MCP Server 2.0 (#452)

* MCP Server 2.0

- Updated application version from 2.23.14 to 2.24.0 in package.json.
- MCP Server improved with new features
- Migrated functionality from Dev Tools to MCP Server
- Improved getMonitoringProvider not to crash application when misconfigured
This commit is contained in:
Giancarlo Buomprisco
2026-02-11 20:42:01 +01:00
committed by GitHub
parent 059408a70a
commit f3ac595d06
123 changed files with 17803 additions and 5265 deletions

View File

@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest';
import {
type DepsUpgradeAdvisorDeps,
createDepsUpgradeAdvisorService,
} from '../deps-upgrade-advisor.service';
function createDeps(
output: unknown,
overrides: Partial<DepsUpgradeAdvisorDeps> = {},
): DepsUpgradeAdvisorDeps {
return {
async executeCommand() {
return {
stdout: JSON.stringify(output),
stderr: '',
exitCode: 0,
};
},
nowIso() {
return '2026-02-09T00:00:00.000Z';
},
...overrides,
};
}
describe('DepsUpgradeAdvisorService', () => {
it('flags major updates as potentially breaking', async () => {
const service = createDepsUpgradeAdvisorService(
createDeps([
{
name: 'zod',
current: '3.25.0',
wanted: '3.26.0',
latest: '4.0.0',
workspace: 'root',
dependencyType: 'dependencies',
},
]),
);
const result = await service.advise({});
const zod = result.recommendations.find((item) => item.package === 'zod');
expect(zod?.update_type).toBe('major');
expect(zod?.potentially_breaking).toBe(true);
expect(zod?.risk).toBe('high');
});
it('prefers wanted for major updates when includeMajor is false', async () => {
const service = createDepsUpgradeAdvisorService(
createDeps([
{
name: 'example-lib',
current: '1.2.0',
wanted: '1.9.0',
latest: '2.1.0',
workspace: 'root',
dependencyType: 'dependencies',
},
]),
);
const result = await service.advise({});
const item = result.recommendations[0];
expect(item?.recommended_target).toBe('1.9.0');
});
it('filters out dev dependencies when requested', async () => {
const service = createDepsUpgradeAdvisorService(
createDeps([
{
name: 'vitest',
current: '2.1.0',
wanted: '2.1.8',
latest: '2.1.8',
workspace: 'root',
dependencyType: 'devDependencies',
},
{
name: 'zod',
current: '3.25.0',
wanted: '3.25.1',
latest: '3.25.1',
workspace: 'root',
dependencyType: 'dependencies',
},
]),
);
const result = await service.advise({
state: { includeDevDependencies: false },
});
expect(result.recommendations).toHaveLength(1);
expect(result.recommendations[0]?.package).toBe('zod');
});
});

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import { registerDepsUpgradeAdvisorToolWithDeps } from '../index';
import { DepsUpgradeAdvisorOutputSchema } from '../schema';
interface RegisteredTool {
name: string;
handler: (input: unknown) => Promise<Record<string, unknown>>;
}
describe('registerDepsUpgradeAdvisorTool', () => {
it('registers deps_upgrade_advisor and returns typed structured output', async () => {
const tools: RegisteredTool[] = [];
const server = {
registerTool(
name: string,
_config: Record<string, unknown>,
handler: (input: unknown) => Promise<Record<string, unknown>>,
) {
tools.push({ name, handler });
return {};
},
};
registerDepsUpgradeAdvisorToolWithDeps(server as never, {
async executeCommand() {
return {
stdout: '[]',
stderr: '',
exitCode: 0,
};
},
nowIso() {
return '2026-02-09T00:00:00.000Z';
},
});
expect(tools).toHaveLength(1);
expect(tools[0]?.name).toBe('deps_upgrade_advisor');
const result = await tools[0]!.handler({});
const parsed = DepsUpgradeAdvisorOutputSchema.parse(
result.structuredContent,
);
expect(parsed.generated_at).toBeTruthy();
expect(Array.isArray(parsed.recommendations)).toBe(true);
});
});

View File

@@ -0,0 +1,307 @@
import type {
DepsUpgradeAdvisorInput,
DepsUpgradeAdvisorOutput,
DepsUpgradeRecommendation,
} from './schema';
interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
interface OutdatedDependency {
package: string;
workspace: string;
dependencyType: string;
current: string;
wanted: string;
latest: string;
}
export interface DepsUpgradeAdvisorDeps {
executeCommand(command: string, args: string[]): Promise<CommandResult>;
nowIso(): string;
}
export function createDepsUpgradeAdvisorService(deps: DepsUpgradeAdvisorDeps) {
return new DepsUpgradeAdvisorService(deps);
}
export class DepsUpgradeAdvisorService {
constructor(private readonly deps: DepsUpgradeAdvisorDeps) {}
async advise(
input: DepsUpgradeAdvisorInput,
): Promise<DepsUpgradeAdvisorOutput> {
const includeMajor = input.state?.includeMajor ?? false;
const maxPackages = input.state?.maxPackages ?? 50;
const includeDevDependencies = input.state?.includeDevDependencies ?? true;
const warnings: string[] = [];
const outdated = await this.getOutdatedDependencies(warnings);
const filtered = outdated.filter((item) => {
if (includeDevDependencies) {
return true;
}
return !item.dependencyType.toLowerCase().includes('dev');
});
const recommendations = filtered
.map((item) => toRecommendation(item, includeMajor))
.sort(sortRecommendations)
.slice(0, maxPackages);
const major = recommendations.filter(
(item) => item.update_type === 'major',
);
const safe = recommendations.filter((item) => item.update_type !== 'major');
if (!includeMajor && major.length > 0) {
warnings.push(
`${major.length} major upgrades were excluded from immediate recommendations. Re-run with includeMajor=true to include them.`,
);
}
return {
generated_at: this.deps.nowIso(),
summary: {
total_outdated: filtered.length,
recommended_now: recommendations.filter((item) =>
includeMajor ? true : item.update_type !== 'major',
).length,
major_available: filtered
.map((item) => toRecommendation(item, true))
.filter((item) => item.update_type === 'major').length,
minor_or_patch_available: filtered
.map((item) => toRecommendation(item, true))
.filter(
(item) =>
item.update_type === 'minor' || item.update_type === 'patch',
).length,
},
recommendations,
grouped_commands: {
safe_batch_command: buildBatchCommand(
safe.map((item) => `${item.package}@${item.recommended_target}`),
),
major_batch_command: includeMajor
? buildBatchCommand(
major.map((item) => `${item.package}@${item.recommended_target}`),
)
: null,
},
warnings,
};
}
private async getOutdatedDependencies(warnings: string[]) {
const attempts: string[][] = [
['outdated', '--recursive', '--format', 'json'],
['outdated', '--recursive', '--json'],
];
let lastError: Error | null = null;
for (const args of attempts) {
const result = await this.deps.executeCommand('pnpm', args);
if (!result.stdout.trim()) {
if (result.exitCode === 0) {
return [] as OutdatedDependency[];
}
warnings.push(
`pnpm ${args.join(' ')} returned no JSON output (exit code ${result.exitCode}).`,
);
lastError = new Error(result.stderr || 'Missing command output');
continue;
}
try {
return normalizeOutdatedJson(JSON.parse(result.stdout));
} catch (error) {
lastError = error instanceof Error ? error : new Error('Invalid JSON');
}
}
throw lastError ?? new Error('Unable to retrieve outdated dependencies');
}
}
function toRecommendation(
dependency: OutdatedDependency,
includeMajor: boolean,
): DepsUpgradeRecommendation {
const updateType = getUpdateType(dependency.current, dependency.latest);
const risk =
updateType === 'major' ? 'high' : updateType === 'minor' ? 'medium' : 'low';
const target =
updateType === 'major' && !includeMajor
? dependency.wanted
: dependency.latest;
return {
package: dependency.package,
workspace: dependency.workspace,
dependency_type: dependency.dependencyType,
current: dependency.current,
wanted: dependency.wanted,
latest: dependency.latest,
update_type: updateType,
risk,
potentially_breaking: updateType === 'major',
recommended_target: target,
recommended_command: `pnpm up -r ${dependency.package}@${target}`,
reason:
updateType === 'major' && !includeMajor
? 'Major version available and potentially breaking; recommended target is the highest non-major range match.'
: `Recommended ${updateType} update based on current vs latest version.`,
};
}
function normalizeOutdatedJson(value: unknown): OutdatedDependency[] {
if (Array.isArray(value)) {
return value.map(normalizeOutdatedItem).filter((item) => item !== null);
}
if (isRecord(value)) {
const rows: OutdatedDependency[] = [];
for (const [workspace, data] of Object.entries(value)) {
if (!isRecord(data)) {
continue;
}
for (const [name, info] of Object.entries(data)) {
if (!isRecord(info)) {
continue;
}
const current = readString(info, 'current');
const wanted = readString(info, 'wanted');
const latest = readString(info, 'latest');
if (!current || !wanted || !latest) {
continue;
}
rows.push({
package: name,
workspace,
dependencyType: readString(info, 'dependencyType') ?? 'unknown',
current,
wanted,
latest,
});
}
}
return rows;
}
return [];
}
function normalizeOutdatedItem(value: unknown): OutdatedDependency | null {
if (!isRecord(value)) {
return null;
}
const name =
readString(value, 'name') ??
readString(value, 'package') ??
readString(value, 'pkgName');
const current = readString(value, 'current');
const wanted = readString(value, 'wanted');
const latest = readString(value, 'latest');
if (!name || !current || !wanted || !latest) {
return null;
}
return {
package: name,
workspace:
readString(value, 'workspace') ??
readString(value, 'dependent') ??
readString(value, 'location') ??
'root',
dependencyType:
readString(value, 'dependencyType') ??
readString(value, 'packageType') ??
'unknown',
current,
wanted,
latest,
};
}
function getUpdateType(current: string, latest: string) {
const currentVersion = parseSemver(current);
const latestVersion = parseSemver(latest);
if (!currentVersion || !latestVersion) {
return 'unknown' as const;
}
if (latestVersion.major > currentVersion.major) {
return 'major' as const;
}
if (latestVersion.minor > currentVersion.minor) {
return 'minor' as const;
}
if (latestVersion.patch > currentVersion.patch) {
return 'patch' as const;
}
return 'unknown' as const;
}
function parseSemver(input: string) {
const match = input.match(/(\d+)\.(\d+)\.(\d+)/);
if (!match) {
return null;
}
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
};
}
function buildBatchCommand(upgrades: string[]) {
if (upgrades.length === 0) {
return null;
}
return `pnpm up -r ${upgrades.join(' ')}`;
}
function sortRecommendations(
a: DepsUpgradeRecommendation,
b: DepsUpgradeRecommendation,
) {
const rank: Record<DepsUpgradeRecommendation['risk'], number> = {
high: 0,
medium: 1,
low: 2,
};
return rank[a.risk] - rank[b.risk] || a.package.localeCompare(b.package);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function readString(record: Record<string, unknown>, key: string) {
const value = record[key];
return typeof value === 'string' && value.length > 0 ? value : null;
}

View File

@@ -0,0 +1,122 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import {
type DepsUpgradeAdvisorDeps,
createDepsUpgradeAdvisorService,
} from './deps-upgrade-advisor.service';
import {
DepsUpgradeAdvisorInputSchema,
DepsUpgradeAdvisorOutputSchema,
} from './schema';
const execFileAsync = promisify(execFile);
export function registerDepsUpgradeAdvisorTool(server: McpServer) {
return registerDepsUpgradeAdvisorToolWithDeps(
server,
createDepsUpgradeAdvisorDeps(),
);
}
export function registerDepsUpgradeAdvisorToolWithDeps(
server: McpServer,
deps: DepsUpgradeAdvisorDeps,
) {
const service = createDepsUpgradeAdvisorService(deps);
return server.registerTool(
'deps_upgrade_advisor',
{
description:
'Analyze outdated dependencies and return risk-bucketed upgrade recommendations',
inputSchema: DepsUpgradeAdvisorInputSchema,
outputSchema: DepsUpgradeAdvisorOutputSchema,
},
async (input) => {
try {
const parsed = DepsUpgradeAdvisorInputSchema.parse(input);
const result = await service.advise(parsed);
return {
structuredContent: result,
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `deps_upgrade_advisor failed: ${toErrorMessage(error)}`,
},
],
};
}
},
);
}
function createDepsUpgradeAdvisorDeps(): DepsUpgradeAdvisorDeps {
const rootPath = process.cwd();
return {
async executeCommand(command, args) {
try {
const result = await execFileAsync(command, args, {
cwd: rootPath,
maxBuffer: 1024 * 1024 * 10,
});
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: 0,
};
} catch (error) {
if (isExecError(error)) {
return {
stdout: error.stdout ?? '',
stderr: error.stderr ?? '',
exitCode: error.code,
};
}
throw error;
}
},
nowIso() {
return new Date().toISOString();
},
};
}
interface ExecError extends Error {
code: number;
stdout?: string;
stderr?: string;
}
function isExecError(error: unknown): error is ExecError {
return error instanceof Error && 'code' in error;
}
function toErrorMessage(error: unknown) {
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
export {
createDepsUpgradeAdvisorService,
type DepsUpgradeAdvisorDeps,
} from './deps-upgrade-advisor.service';
export type { DepsUpgradeAdvisorOutput } from './schema';

View File

@@ -0,0 +1,52 @@
import { z } from 'zod/v3';
export const DepsUpgradeAdvisorInputSchema = z.object({
state: z
.object({
includeMajor: z.boolean().optional(),
maxPackages: z.number().int().min(1).max(200).optional(),
includeDevDependencies: z.boolean().optional(),
})
.optional(),
});
export const DepsUpgradeRecommendationSchema = z.object({
package: z.string(),
workspace: z.string(),
dependency_type: z.string(),
current: z.string(),
wanted: z.string(),
latest: z.string(),
update_type: z.enum(['major', 'minor', 'patch', 'unknown']),
risk: z.enum(['high', 'medium', 'low']),
potentially_breaking: z.boolean(),
recommended_target: z.string(),
recommended_command: z.string(),
reason: z.string(),
});
export const DepsUpgradeAdvisorOutputSchema = z.object({
generated_at: z.string(),
summary: z.object({
total_outdated: z.number().int().min(0),
recommended_now: z.number().int().min(0),
major_available: z.number().int().min(0),
minor_or_patch_available: z.number().int().min(0),
}),
recommendations: z.array(DepsUpgradeRecommendationSchema),
grouped_commands: z.object({
safe_batch_command: z.string().nullable(),
major_batch_command: z.string().nullable(),
}),
warnings: z.array(z.string()),
});
export type DepsUpgradeAdvisorInput = z.infer<
typeof DepsUpgradeAdvisorInputSchema
>;
export type DepsUpgradeRecommendation = z.infer<
typeof DepsUpgradeRecommendationSchema
>;
export type DepsUpgradeAdvisorOutput = z.infer<
typeof DepsUpgradeAdvisorOutputSchema
>;