EOL MCP Server
by ducthinh993
Verified
- src
#!/usr/bin/env node
import { Server, ServerOptions } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
ErrorCode,
McpError,
Implementation,
ServerCapabilities
} from "@modelcontextprotocol/sdk/types.js";
import axios, { AxiosInstance } from "axios";
import {
EOLCycle,
ProductInfo,
CheckVersionArgs,
isValidCheckVersionArgs,
CachedQuery,
isValidListProductsArgs,
CVECheckArgs,
isValidCVECheckArgs,
CVEDetails,
CompareVersionsArgs,
isValidCompareVersionsArgs,
ValidationResult,
VersionValidation,
ValidationsResult,
GetAllDetailsArgs,
isValidGetAllDetailsArgs
} from "./types.js";
const API_CONFIG = {
BASE_URL: 'https://endoflife.date/api',
CVE_BASE_URL: 'https://www.cvedetails.com/json-feed.php',
MAX_CACHED_QUERIES: 5,
ENDPOINTS: {
ALL_PRODUCTS: '/all.json'
}
} as const;
class EOLServer {
private server: Server;
private axiosInstance: AxiosInstance;
private cveAxiosInstance: AxiosInstance;
private recentQueries: CachedQuery[] = [];
private availableProducts: string[] = [];
private static readonly PROMPTS = {
"check_software_status": {
name: "check_software_status",
description: "Check if software versions are supported and get EOL dates",
arguments: [
{
name: "product",
description: "Software product name (e.g., python, nodejs, ubuntu)",
required: true
},
{
name: "version",
description: "Specific version to check (optional)",
required: false
}
]
},
"compare_versions": {
name: "compare_versions",
description: "Compare versions and analyze upgrade recommendations",
arguments: [
{
name: "product",
description: "Software product name (e.g., python, nodejs)",
required: true
},
{
name: "version",
description: "Current version being used",
required: true
}
]
},
"analyze_security": {
name: "analyze_security",
description: "Comprehensive security analysis including EOL status and vulnerabilities",
arguments: [
{
name: "product",
description: "Software product name",
required: true
},
{
name: "version",
description: "Version to analyze",
required: true
}
]
},
"natural_language_query": {
name: "natural_language_query",
description: "Process natural language queries about software lifecycle",
arguments: [
{
name: "query",
description: "Natural language question about software versions, support, or security",
required: true
}
]
},
"validate_version": {
name: "validate_version",
description: "Validate version recommendations before responding",
arguments: [
{
name: "product",
description: "Software product name",
required: true
},
{
name: "versions",
description: "List of versions to validate",
required: true
}
]
}
} as const;
private static readonly PROMPT_TEMPLATES = {
VERSION_VALIDATION: (currentDate: string) => [
"2. VERSION VALIDATION:",
" a. Get All Versions:",
" [Using get_all_details]",
" - Get complete version history",
" - Check all EOL dates",
" - Verify support status",
"",
" b. Version Analysis:",
" [Using check_version]",
" - Validate specific version",
" - Check latest patches",
" - Verify LTS status",
"",
" c. Security Check:",
" [Using check_cve]",
" - Check vulnerabilities",
" - Verify security patches",
" - Validate support"
].join("\n"),
RESPONSE_HEADER: (currentDate: string) => [
"VALIDATION REQUIREMENTS:",
`1. Current date: ${currentDate}`,
""
].join("\n"),
RESPONSE_FORMAT: (currentDate: string) => [
"3. RESPONSE FORMAT:",
" ```",
` Current date: ${currentDate}`,
"",
" Version Analysis:",
" 1. Current Version:",
" - EOL Check: YYYY-MM-DD ({valid|invalid}, {+/-N} days)",
" - Support: {active|inactive}",
" - Security: {supported|unsupported}",
"",
" 2. Latest Available:",
" - Version: X.Y.Z",
" - EOL Date: YYYY-MM-DD",
" - Support: {active|inactive}",
" - LTS: {yes|no}",
"",
" Recommendation:",
" - Upgrade Status: {required|optional|none}",
" - Urgency: {critical|high|medium|low}",
" - Timeline: {immediate|planned|none}",
" ```"
].join("\n")
} as const;
constructor() {
const serverInfo: Implementation = {
name: "eol-mcp-server",
version: "0.1.0"
};
const options = {
capabilities: {
experimental: {},
logging: {},
prompts: {
listChanged: false
},
resources: {},
tools: {}
}
};
this.server = new Server(serverInfo, options);
this.axiosInstance = axios.create({
baseURL: API_CONFIG.BASE_URL,
headers: {
'accept': 'application/json',
'content-type': 'application/json'
}
});
this.cveAxiosInstance = axios.create({
baseURL: API_CONFIG.CVE_BASE_URL,
headers: {
'accept': 'application/json',
'content-type': 'application/json'
}
});
this.setupHandlers();
this.setupErrorHandling();
this.loadAvailableProducts().catch(console.error);
}
private setupHandlers(): void {
this.setupResourceHandlers();
this.setupToolHandlers();
this.setupPromptHandlers();
}
private setupResourceHandlers(): void {
this.server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: this.recentQueries.map((query, index) => ({
uri: `eol://queries/${index}`,
name: `Recent query: ${query.product}${query.version ? ` v${query.version}` : ''}`,
mimeType: "application/json",
description: `EOL status for ${query.product} (${query.timestamp})`
}))
})
);
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
const match = request.params.uri.match(/^eol:\/\/queries\/(\d+)$/);
if (!match) {
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown resource: ${request.params.uri}`
);
}
const index = parseInt(match[1]);
const query = this.recentQueries[index];
if (!query) {
throw new McpError(
ErrorCode.InvalidRequest,
`Query result not found: ${index}`
);
}
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(query.response, null, 2)
}]
};
}
);
}
private setupToolHandlers(): void {
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: "check_version",
description: "Check EOL status and support information for software versions",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description: "Software product name (e.g., python, nodejs, ubuntu)",
examples: ["python", "nodejs", "ubuntu"]
},
version: {
type: "string",
description: "Specific version to check (e.g., 3.8, 16, 20.04)",
examples: ["3.8", "16", "20.04"]
}
},
required: ["product"]
}
},
{
name: "check_cve",
description: "Scan for known security vulnerabilities and support status",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description: "Software product name",
examples: ["python", "nodejs"]
},
version: {
type: "string",
description: "Version to check for vulnerabilities",
examples: ["3.8.0", "16.13.0"]
},
vendor: {
type: "string",
description: "Software vendor (optional)",
examples: ["canonical", "redhat"]
}
},
required: ["product", "version"]
}
},
{
name: "list_products",
description: "Browse or search available software products",
inputSchema: {
type: "object",
properties: {
filter: {
type: "string",
description: "Optional search term to filter products",
examples: ["python", "linux", "database"]
}
}
}
},
{
name: "compare_versions",
description: "Compare versions and get detailed upgrade analysis",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description: "Software product name (e.g., python, nodejs)",
examples: ["python", "nodejs"]
},
version: {
type: "string",
description: "Current version being used",
examples: ["3.8", "16"]
}
},
required: ["product", "version"]
}
},
{
name: "get_all_details",
description: "Get comprehensive lifecycle details for all versions of a product",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description: "Software product name (e.g., python, nodejs)",
examples: ["python", "nodejs"]
}
},
required: ["product"]
}
}
]
})
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
const toolName = request.params.name;
const args = request.params.arguments || {};
switch (toolName) {
case "check_version":
if (!isValidCheckVersionArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid version check arguments"
);
}
return this.handleCheckVersion(args);
case "check_cve":
if (!isValidCVECheckArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid CVE check arguments"
);
}
return this.handleCheckCVE(args);
case "list_products":
if (!isValidListProductsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid list products arguments"
);
}
return this.handleListProducts(args);
case "compare_versions": {
if (!isValidCompareVersionsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid version comparison arguments"
);
}
return this.handleCompareVersions(args);
}
case "get_all_details": {
if (!isValidGetAllDetailsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid get all details arguments"
);
}
return this.handleGetAllDetails(args);
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${toolName}`
);
}
}
);
}
private setupPromptHandlers(): void {
this.server.setRequestHandler(
ListPromptsRequestSchema,
async () => ({
prompts: Object.values(EOLServer.PROMPTS)
})
);
this.server.setRequestHandler(
GetPromptRequestSchema,
async (request) => {
const promptName = request.params.name;
const args = request.params.arguments || {};
const currentDate = new Date().toISOString();
switch (promptName) {
case "check_software_status": {
const { product, version } = args;
return {
messages: [{
role: "user",
content: {
type: "text",
text: [
`I'll analyze the software lifecycle status for ${product}${version ? ` version ${version}` : ''}.`,
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_HEADER(currentDate),
EOLServer.PROMPT_TEMPLATES.VERSION_VALIDATION(currentDate),
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_FORMAT(currentDate),
"",
"Let me validate the version status..."
].join("\n")
}
}]
};
}
case "compare_versions": {
const { product, version } = args;
return {
messages: [{
role: "user",
content: {
type: "text",
text: [
`I'll analyze ${product} version ${version} and provide upgrade recommendations.`,
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_HEADER(currentDate),
EOLServer.PROMPT_TEMPLATES.VERSION_VALIDATION(currentDate),
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_FORMAT(currentDate),
"",
"Let me analyze the versions..."
].join("\n")
}
}]
};
}
case "analyze_security": {
const { product, version } = args;
return {
messages: [{
role: "user",
content: {
type: "text",
text: [
`I'll analyze security status for ${product} version ${version}.`,
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_HEADER(currentDate),
EOLServer.PROMPT_TEMPLATES.VERSION_VALIDATION(currentDate),
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_FORMAT(currentDate),
"",
"Let me analyze the security status..."
].join("\n")
}
}]
};
}
case "validate_version": {
const { product, versions } = args;
return {
messages: [{
role: "user",
content: {
type: "text",
text: [
`I'll validate ${product} versions: ${Array.isArray(versions) ? versions.join(", ") : versions}`,
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_HEADER(currentDate),
EOLServer.PROMPT_TEMPLATES.VERSION_VALIDATION(currentDate),
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_FORMAT(currentDate),
"",
"Let me validate each version..."
].join("\n")
}
}]
};
}
case "natural_language_query": {
const { query } = args;
return {
messages: [{
role: "user",
content: {
type: "text",
text: [
`I'll help analyze software lifecycle information. Here's what I found about: ${query}`,
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_HEADER(currentDate),
EOLServer.PROMPT_TEMPLATES.VERSION_VALIDATION(currentDate),
"",
EOLServer.PROMPT_TEMPLATES.RESPONSE_FORMAT(currentDate),
"",
"Let me analyze your query..."
].join("\n")
}
}]
};
}
default:
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown prompt: ${promptName}`
);
}
}
);
}
private async loadAvailableProducts(): Promise<void> {
try {
const response = await this.axiosInstance.get(API_CONFIG.ENDPOINTS.ALL_PRODUCTS);
this.availableProducts = response.data as string[];
} catch (error) {
console.error('Failed to load available products:', error);
this.availableProducts = [];
}
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
public async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("EOL MCP server running on stdio");
}
private async handleCheckVersion(args: CheckVersionArgs) {
const { product, version } = args;
// Validate product exists
if (!this.availableProducts.includes(product)) {
return {
content: [{
type: "text",
text: `Invalid product: ${product}. Use list_products tool to see available products.`
}],
isError: true
};
}
try {
const response = await this.axiosInstance.get(`/${product}.json`);
const cycles = response.data as EOLCycle[];
const filteredCycles = version
? cycles.filter(cycle => cycle.cycle.startsWith(version))
: cycles;
this.recentQueries.unshift({
product,
version,
response: filteredCycles,
timestamp: new Date().toISOString()
});
if (this.recentQueries.length > API_CONFIG.MAX_CACHED_QUERIES) {
this.recentQueries.pop();
}
return {
content: [{
type: "text",
text: JSON.stringify(filteredCycles, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
return {
content: [{
type: "text",
text: `EOL API error: ${error.response?.data?.message ?? error.message}`
}],
isError: true
};
}
throw error;
}
}
private async handleListProducts(args: { filter?: string }) {
const { filter } = args;
let products = this.availableProducts;
if (filter) {
products = products.filter(p =>
p.toLowerCase().includes(filter.toLowerCase())
);
}
return {
content: [{
type: "text",
text: JSON.stringify(products, null, 2)
}]
};
}
private async handleCheckCVE(args: CVECheckArgs) {
const { product, version, vendor } = args;
try {
const response = await this.axiosInstance.get(`/${product}.json`);
const cycles = response.data as EOLCycle[];
const matchingCycle = cycles.find(cycle => cycle.cycle.startsWith(version));
if (!matchingCycle) {
return {
content: [{
type: "text",
text: `Version ${version} not found for ${product}`
}],
isError: true
};
}
// For now, return basic EOL info since we removed Snyk
return {
content: [{
type: "text",
text: JSON.stringify({
product,
version,
vendor,
cycle: matchingCycle,
securityStatus: matchingCycle.support ? 'supported' : 'unsupported'
}, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
return {
content: [{
type: "text",
text: `API error: ${error.response?.data?.message ?? error.message}`
}],
isError: true
};
}
throw error;
}
}
private async handleCompareVersions(args: CompareVersionsArgs) {
const { product, version } = args;
// Validate product exists
if (!this.availableProducts.includes(product)) {
return {
content: [{
type: "text",
text: `Invalid product: ${product}. Use list_products tool to see available products.`
}],
isError: true
};
}
try {
const cycles = await this.getProductDetails(product);
const currentDate = new Date();
// Validate current version
const currentCycle = cycles.find(c => c?.cycle?.startsWith(version));
if (!currentCycle) {
return {
content: [{
type: "text",
text: `Version ${version} not found for ${product}`
}],
isError: true
};
}
// Find and validate latest supported version
const latestSupportedCycle = cycles.find(c => {
const validation = this.validateVersion(c, currentDate);
return validation.isValid && validation.isSupported;
}) || cycles[0];
// Validate both versions
const currentValidation = this.validateVersion(currentCycle, currentDate);
const latestValidation = this.validateVersion(latestSupportedCycle, currentDate);
// Cache the query
this.recentQueries.unshift({
product,
version,
response: [currentCycle, latestSupportedCycle],
timestamp: currentDate.toISOString()
});
if (this.recentQueries.length > API_CONFIG.MAX_CACHED_QUERIES) {
this.recentQueries.pop();
}
const response = {
current_date: currentDate.toISOString(),
validations: {
current: this.formatVersionValidation(currentCycle, currentValidation),
latest: this.formatVersionValidation(latestSupportedCycle, latestValidation)
},
recommendation: {
needs_update: !currentValidation.isValid || !currentValidation.isSupported,
urgency: this.getUpgradeUrgency(currentValidation.daysToEol),
message: this.getRecommendationMessage(currentValidation)
}
};
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
return {
content: [{
type: "text",
text: `API error: ${error.response?.data?.message ?? error.message}`
}],
isError: true
};
}
throw error;
}
}
private formatVersionValidation(cycle: EOLCycle, validation: ValidationResult) {
return {
version: cycle.cycle,
eol_check: {
date: cycle.eol,
valid: validation.isValid,
days_remaining: validation.daysToEol,
message: validation.validationMessage
},
support: {
status: validation.isSupported ? "supported" : "not supported",
lts: this.isValueTruthy(cycle.lts) ? "LTS" : "not LTS"
}
};
}
private getUpgradeUrgency(daysToEol: number): string {
if (daysToEol < 0) return "critical";
if (daysToEol < 30) return "high";
if (daysToEol < 90) return "medium";
return "low";
}
private getRecommendationMessage(validation: ValidationResult): string {
return validation.isSupported && validation.isValid
? "Current version is supported, but consider upgrading to latest for security updates"
: "Current version needs urgent upgrade - use a supported version";
}
// Helper function to check if a value is truthy
private isValueTruthy(value: string | boolean | undefined): boolean {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const lowered = value.toLowerCase();
return lowered === "true" || lowered === "yes";
}
return false;
}
private async getProductDetails(product: string): Promise<EOLCycle[]> {
const response = await this.axiosInstance.get(`/${product}.json`);
return response.data as EOLCycle[];
}
private validateVersion(cycle: EOLCycle | undefined, currentDate: Date = new Date()): ValidationResult {
if (!cycle?.eol) {
return {
isValid: false,
daysToEol: 0,
isSupported: false,
validationMessage: `Invalid cycle data for version ${cycle?.cycle ?? 'unknown'}`
};
}
const eolDate = new Date(cycle.eol);
const daysToEol = Math.floor((eolDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24));
const isSupported = this.isValueTruthy(cycle.support);
return {
isValid: daysToEol > 0,
daysToEol,
isSupported,
validationMessage: `Version ${cycle.cycle} EOL date ${cycle.eol} is ${daysToEol > 0 ? 'valid' : 'invalid'}, ${daysToEol > 0 ? '+' : ''}${daysToEol} days from now`
};
}
private async validateVersions(product: string, versions: string[]): Promise<ValidationsResult> {
const cycles = await this.getProductDetails(product);
const currentDate = new Date();
const validations: Record<string, VersionValidation> = {};
const validVersions: string[] = [];
for (const version of versions) {
const cycle = cycles.find(c => c?.cycle?.startsWith(version));
if (!cycle) continue;
const validation = this.validateVersion(cycle, currentDate);
const securityCheck = await this.handleCheckCVE({ product, version });
validations[version] = {
eol: {
date: cycle.eol,
valid: validation.isValid,
daysRemaining: validation.daysToEol,
message: validation.validationMessage
},
support: {
isSupported: validation.isSupported,
message: `Version ${version} support status: ${validation.isSupported ? 'active' : 'inactive'}`
},
security: {
isSupported: !cycle.eol || new Date(cycle.eol) > currentDate,
message: `Version ${version} security status: ${!cycle.eol || new Date(cycle.eol) > currentDate ? 'supported' : 'unsupported'}`
}
};
if (validation.isValid && validation.isSupported) {
validVersions.push(version);
}
}
return { validations, validVersions };
}
private async handleGetAllDetails(args: GetAllDetailsArgs) {
const { product } = args;
if (!this.availableProducts.includes(product)) {
return {
content: [{
type: "text",
text: `Invalid product: ${product}. Use list_products tool to see available products.`
}],
isError: true
};
}
try {
const cycles = await this.getProductDetails(product);
const currentDate = new Date();
// Add validation results for each cycle
const detailedCycles = cycles.map(cycle => {
const validation = this.validateVersion(cycle, currentDate);
return {
...cycle,
validation: {
is_valid: validation.isValid,
days_to_eol: validation.daysToEol,
is_supported: validation.isSupported,
message: validation.validationMessage
}
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
product,
current_date: currentDate.toISOString(),
cycles: detailedCycles
}, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
return {
content: [{
type: "text",
text: `API error: ${error.response?.data?.message ?? error.message}`
}],
isError: true
};
}
throw error;
}
}
}
const server = new EOLServer();
server.start().catch(console.error);