#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {
ProductSummary,
ProductDetails,
ProductRelease,
Category,
Tag,
ListProductsArgs,
GetProductArgs,
GetProductReleaseArgs,
GetCategoryProductsArgs,
GetTagProductsArgs,
ApiResponse,
ProductListResponse,
ProductListFullResponse,
ProductResponse,
ProductReleaseResponse,
CategoriesResponse,
CategoryProductsResponse,
TagsResponse,
TagProductsResponse,
EndOfLifeApiError,
ValidationError,
} from "./types.js";
const BASE_URL = "https://endoflife.date/api/v1";
class EndOfLifeServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "endoflife-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
this.setupToolHandlers();
}
private async fetchAPI<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${BASE_URL}${endpoint}`);
if (!response.ok) {
throw new EndOfLifeApiError(
`API request failed: ${response.status} ${response.statusText}`,
response.status,
response.statusText,
);
}
return response.json() as Promise<ApiResponse<T>>;
}
private validateArgs<T extends Record<string, unknown>>(
args: unknown,
requiredFields: (keyof T)[],
): T {
if (!args || typeof args !== "object") {
throw new ValidationError("Arguments must be an object", "args");
}
const typedArgs = args as T;
for (const field of requiredFields) {
if (
!(field in typedArgs) ||
typedArgs[field] === undefined ||
typedArgs[field] === null
) {
throw new ValidationError(
`Missing required field: ${String(field)}`,
String(field),
);
}
if (
typeof typedArgs[field] === "string" &&
(typedArgs[field] as string).trim() === ""
) {
throw new ValidationError(
`Field cannot be empty: ${String(field)}`,
String(field),
);
}
}
return typedArgs;
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "list_products",
description:
"Browse all software products tracked for end-of-life information. Use this as your starting point to discover what products are available, or when you need to help users find software they're asking about. Returns product names, descriptions, and basic metadata.",
inputSchema: {
type: "object",
properties: {
full: {
type: "boolean",
description:
"When true, returns complete product details including all release cycles and dates. When false (default), returns just basic product information. Use full=true only when you need comprehensive data about multiple products at once.",
default: false,
},
},
},
},
{
name: "get_product",
description:
"Get comprehensive end-of-life information for a specific software product. Returns all release cycles, support dates, and EOL status. Perfect for answering questions like 'When does Python 3.8 reach end-of-life?' or 'What Ubuntu versions are still supported?'",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description:
"The product identifier exactly as it appears in the endoflife.date database. Common examples: 'python', 'nodejs', 'ubuntu', 'windows', 'mysql', 'postgresql', 'kubernetes', 'docker'. Use list_products first if you're unsure of the exact identifier.",
},
},
required: ["product"],
},
},
{
name: "get_product_release",
description:
"Get detailed information about a specific version/release of a product. Use this when you need precise details about one particular version, such as exact end-of-life dates, support status, or the latest patch version available.",
inputSchema: {
type: "object",
properties: {
product: {
type: "string",
description:
"The product identifier (get from list_products or get_product)",
},
release: {
type: "string",
description:
"Specific release cycle identifier (e.g., '3.8' for Python, '18' for Node.js, '20.04' for Ubuntu) OR use 'latest' to get information about the most recent release. The exact format varies by product - check get_product output first to see available release cycles.",
},
},
required: ["product", "release"],
},
},
{
name: "list_categories",
description:
"Discover how products are organized by category. Returns categories like 'os' (operating systems), 'lang' (programming languages), 'db' (databases), 'server-app' (server applications), etc. Use this to understand the taxonomy or to help users explore related products.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_category_products",
description:
"Find all products within a specific category. Useful for comparative analysis or when users ask broad questions like 'What databases are tracked?' or 'Show me all the operating systems.' Much more focused than browsing all products.",
inputSchema: {
type: "object",
properties: {
category: {
type: "string",
description:
"Category identifier from list_categories. Examples: 'os' for operating systems, 'lang' for programming languages, 'db' for databases, 'framework' for web frameworks, 'server-app' for server applications.",
},
},
required: ["category"],
},
},
{
name: "list_tags",
description:
"Explore products by tags/labels that describe their characteristics. Tags provide a different organizational system than categories, often describing technical attributes, company associations, or use cases.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_tag_products",
description:
"Find all products that share a common tag. Useful for discovering related technologies or when users want to see all products from a specific vendor or with particular characteristics.",
inputSchema: {
type: "object",
properties: {
tag: {
type: "string",
description:
"Tag identifier from list_tags. Tags often represent vendors (e.g., 'microsoft', 'google'), technology types, or other groupings.",
},
},
required: ["tag"],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_products": {
const validatedArgs = (args || {}) as ListProductsArgs;
const endpoint = validatedArgs.full
? "/products/full"
: "/products";
const products = validatedArgs.full
? await this.fetchAPI<ProductListFullResponse>(endpoint)
: await this.fetchAPI<ProductListResponse>(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(products, null, 2),
},
],
};
}
case "get_product": {
const validatedArgs = this.validateArgs<GetProductArgs>(args, [
"product",
]);
const productData = await this.fetchAPI<ProductResponse>(
`/products/${validatedArgs.product}`,
);
return {
content: [
{
type: "text",
text: JSON.stringify(productData, null, 2),
},
],
};
}
case "get_product_release": {
const validatedArgs = this.validateArgs<GetProductReleaseArgs>(
args,
["product", "release"],
);
const endpoint =
validatedArgs.release === "latest"
? `/products/${validatedArgs.product}/releases/latest`
: `/products/${validatedArgs.product}/releases/${validatedArgs.release}`;
const releaseData =
await this.fetchAPI<ProductReleaseResponse>(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(releaseData, null, 2),
},
],
};
}
case "list_categories": {
const categories =
await this.fetchAPI<CategoriesResponse>("/categories");
return {
content: [
{
type: "text",
text: JSON.stringify(categories, null, 2),
},
],
};
}
case "get_category_products": {
const validatedArgs = this.validateArgs<GetCategoryProductsArgs>(
args,
["category"],
);
const products = await this.fetchAPI<CategoryProductsResponse>(
`/categories/${validatedArgs.category}`,
);
return {
content: [
{
type: "text",
text: JSON.stringify(products, null, 2),
},
],
};
}
case "list_tags": {
const tags = await this.fetchAPI<TagsResponse>("/tags");
return {
content: [
{
type: "text",
text: JSON.stringify(tags, null, 2),
},
],
};
}
case "get_tag_products": {
const validatedArgs = this.validateArgs<GetTagProductsArgs>(args, [
"tag",
]);
const products = await this.fetchAPI<TagProductsResponse>(
`/tags/${validatedArgs.tag}`,
);
return {
content: [
{
type: "text",
text: JSON.stringify(products, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
let errorMessage: string;
if (error instanceof EndOfLifeApiError) {
errorMessage = `API Error (${error.status}): ${error.message}`;
} else if (error instanceof ValidationError) {
errorMessage = `Validation Error for field '${error.field}': ${error.message}`;
} else if (error instanceof Error) {
errorMessage = `Error: ${error.message}`;
} else {
errorMessage = `Unknown error: ${String(error)}`;
}
return {
content: [
{
type: "text",
text: errorMessage,
},
],
isError: true,
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}
const server = new EndOfLifeServer();
server.run().catch(console.error);