Skip to main content
Glama
Panth1823

Formula1 MCP Server

index.ts27 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import { F1DataService } from "./services/f1-data.service.js"; import { z } from "zod"; import { config } from "./config/index.js"; import { logger } from "./utils/logger.js"; import { requestIdMiddleware, RequestWithId } from "./middleware/request-id.js"; import { rateLimitMiddleware } from "./middleware/rate-limiter.js"; import { authMiddleware } from "./middleware/auth.js"; import { validateBodyMiddleware, validateQueryMiddleware } from "./middleware/validator.js"; import { setupGracefulShutdown } from "./utils/graceful-shutdown.js"; import { metrics, trackRequestMetrics } from "./utils/metrics.js"; const f1Service = F1DataService.getInstance(); // Helper function to format MCP responses with proper error handling function formatMCPResponse(data: any, context?: string) { try { // Handle null or undefined if (data === null || data === undefined) { return { content: [{ type: "text" as const, text: JSON.stringify({ message: "No data available", context: context || "unknown" }) }] }; } // Handle empty arrays if (Array.isArray(data) && data.length === 0) { return { content: [{ type: "text" as const, text: JSON.stringify({ message: "No data available", context: context || "query returned empty results", suggestion: context?.includes('live') ? "Live data is only available during active F1 sessions. Use historical data tools instead." : "Try adjusting your query parameters or check data availability." }) }] }; } // Ensure text is a string const textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2); return { content: [{ type: "text" as const, text: textContent }] }; } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Response formatting error", message: error.message, context: context || "unknown" }) }] }; } } // Helper function to register all tools on a server instance function registerAllTools(mcpServer: McpServer) { // Live data endpoints mcpServer.tool("getLiveTimingData", {}, async () => { const data = await f1Service.getLiveTimingData(); if (data.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ message: "No live F1 session data available at the moment. Please check back during a race weekend.", }), }, ], }; } return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getCurrentSessionStatus", {}, async () => { const data = await f1Service.getCurrentSessionStatus(); return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getDriverInfo", { driverId: z.string() }, async ({ driverId }) => { const data = await f1Service.getDriverInfo(driverId); return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); // Tool to find historical session keys mcpServer.tool( "getHistoricalSessions", { year: z.number().optional(), circuit_short_name: z.string().optional(), session_name: z.string().optional(), country_name: z.string().optional(), location: z.string().optional(), }, async (filters) => { const data = await f1Service.getHistoricalSessions(filters); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); // Historic data endpoints mcpServer.tool( "getHistoricRaceResults", { year: z.number(), round: z.number(), }, async ({ year, round }) => { try { const data = await f1Service.getHistoricRaceResults(year, round); return { content: [{ type: "text" as const, text: JSON.stringify(data) }], }; } catch (error: any) { logger.error('getHistoricRaceResults error', { year, round, error: error.message }); // Return empty race object structure return { content: [{ type: "text" as const, text: JSON.stringify({ Results: [] }) }], }; } } ); mcpServer.tool( "getDriverStandings", { year: z.number(), }, async ({ year }) => { const data = await f1Service.getDriverStandings(year); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getConstructorStandings", { year: z.number(), }, async ({ year }) => { const data = await f1Service.getConstructorStandings(year); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getLapTimes", { year: z.number(), round: z.number(), driverId: z.string(), }, async ({ year, round, driverId }) => { const data = await f1Service.getLapTimes(year, round, driverId); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); // New OpenF1 API tools mcpServer.tool( "getWeatherData", { sessionKey: z.string().describe('Session key from getHistoricalSessions. Required for accurate weather data.'), }, async ({ sessionKey }) => { const data = await f1Service.getWeatherData(sessionKey); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getCarData", { driverNumber: z.string(), sessionKey: z.string().describe('Session key from getHistoricalSessions. Required for car telemetry data.'), filters: z.string().optional(), }, async ({ driverNumber, sessionKey, filters }) => { const data = await f1Service.getCarData(driverNumber, sessionKey, filters); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getPitStopData", { sessionKey: z.string().optional(), driverNumber: z.string().optional(), }, async ({ sessionKey, driverNumber }) => { const data = await f1Service.getPitStopData(sessionKey, driverNumber); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getTeamRadio", { sessionKey: z.string().describe('Session key from getHistoricalSessions'), driverNumber: z.string().optional().describe('Filter by specific driver number'), }, async ({ sessionKey, driverNumber }) => { const data = await f1Service.getTeamRadio( sessionKey, driverNumber || "" ); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getRaceControlMessages", { sessionKey: z.string().describe('Session key from getHistoricalSessions'), }, async ({ sessionKey }) => { const data = await f1Service.getRaceControlMessages(sessionKey); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); // New Ergast API tools mcpServer.tool( "getRaceCalendar", { year: z.number(), }, async ({ year }) => { const data = await f1Service.getRaceCalendar(year); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getCircuitInfo", { circuitId: z.string(), }, async ({ circuitId }) => { const data = await f1Service.getCircuitInfo(circuitId); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getSeasonList", { limit: z.number().optional(), }, async ({ limit }) => { const data = await f1Service.getSeasonList(limit); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getQualifyingResults", { year: z.number(), round: z.number(), }, async ({ year, round }) => { try { const data = await f1Service.getQualifyingResults(year, round); return { content: [{ type: "text" as const, text: JSON.stringify(data) }], }; } catch (error: any) { logger.error('getQualifyingResults error', { year, round, error: error.message }); // Return empty qualifying object structure return { content: [{ type: "text" as const, text: JSON.stringify({ QualifyingResults: [] }) }], }; } } ); mcpServer.tool( "getDriverInformation", { driverId: z.string(), }, async ({ driverId }) => { const data = await f1Service.getDriverInformation(driverId); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); mcpServer.tool( "getConstructorInformation", { constructorId: z.string(), }, async ({ constructorId }) => { const data = await f1Service.getConstructorInformation(constructorId); return { content: [{ type: "text", text: JSON.stringify(data) }], }; } ); // Utility tools mcpServer.tool("clearCache", {}, async () => { f1Service.clearCache(); return { content: [ { type: "text", text: JSON.stringify({ message: "Cache cleared successfully" }), }, ], }; }); // New Live Streaming Tools mcpServer.tool("getLiveCarData", {}, async () => { const data = await f1Service.getLiveCarData(); // Service returns empty array if no live data, which is valid return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getLivePositions", {}, async () => { const data = await f1Service.getLivePositions(); // Service returns empty array if no live data, which is valid return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getLiveRaceControl", {}, async () => { const data = await f1Service.getLiveRaceControl(); return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getLiveTeamRadio", {}, async () => { const data = await f1Service.getLiveTeamRadio(); return { content: [{ type: "text", text: JSON.stringify(data) }], }; }); mcpServer.tool("getLiveWeather", {}, async () => { const data = await f1Service.getLiveWeather(); // Service returns null if no live data, which is valid return formatMCPResponse(data || { message: "No weather data available" }, "live weather"); }); } export function createServer(): McpServer { const server = new McpServer({ name: "f1-mcp-server", version: "1.0.0", }); registerAllTools(server); return server; } // Optional standalone stdio bootstrap for local development. // Enable by running with MCP_STANDALONE=1 if (process.env.MCP_STANDALONE === "1") { const server = createServer(); logger.info("Starting F1 MCP Server (stdio mode)..."); const transport = new StdioServerTransport(); (async () => { await server.connect(transport); logger.info("F1 MCP Server connected (stdio mode)"); })(); process.on("uncaughtException", (err) => { logger.error("Uncaught exception", { error: err.message, stack: err.stack, }); }); process.on("unhandledRejection", (reason) => { logger.error("Unhandled rejection", { reason: reason instanceof Error ? reason.message : String(reason), }); }); } // HTTP server mode for MCP over HTTP transport // Enabled when PORT is provided (typical deployment environments) if (process.env.PORT || config.port) { const port = config.port; const app = express(); // Trust proxy (for rate limiting IP detection) app.set('trust proxy', true); // Middleware setup app.use(express.json({ limit: "1mb" })); app.use(requestIdMiddleware); app.use(trackRequestMetrics); app.use(validateBodyMiddleware); app.use(validateQueryMiddleware); // CORS if (config.enableCors) { app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', config.corsOrigin); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Request-Id'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); } // Rate limiting (before auth to prevent brute force) app.use(rateLimitMiddleware); // Authentication (only for API endpoints, not health checks) app.use((req, res, next) => { // Skip auth for health and well-known endpoints if (req.path.startsWith('/health') || req.path.startsWith('/.well-known')) { return next(); } authMiddleware(req, res, next); }); // Health check endpoints app.get("/health", (req: RequestWithId, res) => { res.status(200).json({ status: "ok", timestamp: new Date().toISOString(), requestId: req.requestId, }); }); // Liveness probe - simple check that server is running app.get("/health/live", (req: RequestWithId, res) => { res.status(200).json({ status: "alive", timestamp: new Date().toISOString(), requestId: req.requestId, }); }); // Readiness probe - check if server is ready to accept requests app.get("/health/ready", (req: RequestWithId, res) => { // Add checks for dependencies (Redis, Postgres, etc.) here const checks: Record<string, boolean> = { server: true, // Add more checks as needed // redis: redisClient.isReady(), // postgres: postgresClient.isConnected(), }; const allReady = Object.values(checks).every(v => v === true); if (allReady) { res.status(200).json({ status: "ready", checks, timestamp: new Date().toISOString(), requestId: req.requestId, }); } else { res.status(503).json({ status: "not ready", checks, timestamp: new Date().toISOString(), requestId: req.requestId, }); } }); // Metrics endpoint (Prometheus format) if (config.metricsEnabled) { app.get("/metrics", (_req, res) => { res.set('Content-Type', 'text/plain'); res.send(metrics.exportPrometheus()); }); } // Well-known MCP config app.get("/.well-known/mcp-config", (_req, res) => { res.json({ name: "f1-mcp-server", version: "1.0.0", apiVersion: config.apiVersion, endpoint: `/mcp/${config.apiVersion}`, }); }); // MCP JSON-RPC endpoint app.post(`/mcp/${config.apiVersion}`, async (req: RequestWithId, res) => { const body = req.body ?? {}; const id = body.id ?? null; const method = body.method ?? ""; const params = body.params ?? {}; logger.debug('MCP request', { requestId: req.requestId, method, id, }); if (method === "initialize") { return res.json({ jsonrpc: "2.0", id, result: { protocolVersion: "2025-06-18", serverInfo: { name: "f1-mcp-server", version: "1.0.0" }, capabilities: {}, }, }); } if (method === "tools/list") { // Return list of available tools - manually maintained list const tools = [ { name: "getLiveTimingData", description: "Get live F1 timing data", inputSchema: { type: "object", properties: {} } }, { name: "getCurrentSessionStatus", description: "Get current F1 session status", inputSchema: { type: "object", properties: {} } }, { name: "getDriverInfo", description: "Get driver bio information (Ergast API)", inputSchema: { type: "object", properties: { driverId: { type: "string", description: "Driver ID (e.g., 'hamilton', 'verstappen')" } }, required: ["driverId"] } }, { name: "getHistoricalSessions", description: "Find historical F1 sessions", inputSchema: { type: "object", properties: {} } }, { name: "getHistoricRaceResults", description: "Get historic race results", inputSchema: { type: "object", properties: { year: { type: "number" }, round: { type: "number" } }, required: ["year", "round"] } }, { name: "getDriverStandings", description: "Get driver standings", inputSchema: { type: "object", properties: { year: { type: "number" } }, required: ["year"] } }, { name: "getConstructorStandings", description: "Get constructor standings", inputSchema: { type: "object", properties: { year: { type: "number" } }, required: ["year"] } }, { name: "getWeatherData", description: "Get weather data", inputSchema: { type: "object", properties: { sessionKey: { type: "string" } }, required: ["sessionKey"] } }, { name: "getCarData", description: "Get car telemetry data (requires sessionKey, auto-adds speed>=0 filter)", inputSchema: { type: "object", properties: { sessionKey: { type: "string", description: "Required: Session key to query" }, driverNumber: { type: "string", description: "Driver number" }, filters: { type: "string", description: "Optional filters like 'speed>=300' or 'lap_number=1'" } }, required: ["sessionKey", "driverNumber"] } }, { name: "getPitStopData", description: "Get pit stop data", inputSchema: { type: "object", properties: { sessionKey: { type: "string" } }, required: ["sessionKey"] } }, { name: "getTeamRadio", description: "Get team radio communications (may not be available for all historical sessions)", inputSchema: { type: "object", properties: { sessionKey: { type: "string", description: "Session key" }, driverNumber: { type: "string", description: "Optional: Filter by specific driver number" } }, required: ["sessionKey"] } }, { name: "getRaceControlMessages", description: "Get race control messages", inputSchema: { type: "object", properties: { sessionKey: { type: "string" } }, required: ["sessionKey"] } }, { name: "getRaceCalendar", description: "Get F1 race calendar", inputSchema: { type: "object", properties: { year: { type: "number" } }, required: ["year"] } }, { name: "getLapTimes", description: "Get lap times", inputSchema: { type: "object", properties: { year: { type: "number" }, round: { type: "number" }, driverId: { type: "string" } }, required: ["year", "round", "driverId"] } }, { name: "getQualifyingResults", description: "Get qualifying results", inputSchema: { type: "object", properties: { year: { type: "number" }, round: { type: "number" } }, required: ["year", "round"] } }, { name: "getCircuitInfo", description: "Get circuit information", inputSchema: { type: "object", properties: { circuitId: { type: "string" } }, required: ["circuitId"] } }, { name: "getSeasonList", description: "Get list of F1 seasons", inputSchema: { type: "object", properties: {} } }, { name: "getDriverInformation", description: "Get driver information", inputSchema: { type: "object", properties: { driverId: { type: "string" } }, required: ["driverId"] } }, { name: "getConstructorInformation", description: "Get constructor information", inputSchema: { type: "object", properties: { constructorId: { type: "string" } }, required: ["constructorId"] } }, { name: "clearCache", description: "Clear server cache", inputSchema: { type: "object", properties: {} } }, ]; return res.json({ jsonrpc: "2.0", id, result: { tools }, }); } if (method === "tools/call") { try { const toolName = params.name; const toolArgs = params.arguments || {}; let result; // Execute tool based on name switch (toolName) { case "getLiveTimingData": { const data = await f1Service.getLiveTimingData(); result = { content: [{ type: "text", text: JSON.stringify(data.length > 0 ? data : { message: "No live F1 session data available" }) }], }; break; } case "getCurrentSessionStatus": { const data = await f1Service.getCurrentSessionStatus(); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getDriverInfo": { const data = await f1Service.getDriverInfo(toolArgs.driverId); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getHistoricalSessions": { const data = await f1Service.getHistoricalSessions(toolArgs); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getHistoricRaceResults": { const data = await f1Service.getHistoricRaceResults(toolArgs.year, toolArgs.round); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getDriverStandings": { const data = await f1Service.getDriverStandings(toolArgs.year); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getConstructorStandings": { const data = await f1Service.getConstructorStandings(toolArgs.year); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getWeatherData": { const data = await f1Service.getWeatherData(toolArgs.sessionKey); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getCarData": { const data = await f1Service.getCarData(toolArgs.driverNumber, toolArgs.sessionKey, toolArgs.filters); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getPitStopData": { const data = await f1Service.getPitStopData(toolArgs.sessionKey, toolArgs.driverNumber); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getTeamRadio": { const data = await f1Service.getTeamRadio(toolArgs.sessionKey, toolArgs.driverNumber); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getRaceControlMessages": { const data = await f1Service.getRaceControlMessages(toolArgs.sessionKey); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getRaceCalendar": { const data = await f1Service.getRaceCalendar(toolArgs.year); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getLapTimes": { const data = await f1Service.getLapTimes(toolArgs.year, toolArgs.round, toolArgs.driverId); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getQualifyingResults": { const data = await f1Service.getQualifyingResults(toolArgs.year, toolArgs.round); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getCircuitInfo": { const data = await f1Service.getCircuitInfo(toolArgs.circuitId); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getSeasonList": { const data = await f1Service.getSeasonList(toolArgs.limit); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getDriverInformation": { const data = await f1Service.getDriverInformation(toolArgs.driverId); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "getConstructorInformation": { const data = await f1Service.getConstructorInformation(toolArgs.constructorId); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } case "clearCache": { const data = await f1Service.clearCache(); result = { content: [{ type: "text", text: JSON.stringify(data) }] }; break; } default: throw new Error(`Unknown tool: ${toolName}`); } return res.json({ jsonrpc: "2.0", id, result, }); } catch (error: any) { logger.error('Tool call error', { requestId: req.requestId, error: error.message, stack: error.stack, }); return res.json({ jsonrpc: "2.0", id, error: { code: -32000, message: error.message || "Tool execution failed", }, }); } } return res.status(200).json({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" }, }); }); // 404 handler app.use((req: RequestWithId, res) => { res.status(404).json({ error: "Not Found", message: `Route ${req.method} ${req.path} not found`, requestId: req.requestId, }); }); // Error handler app.use((err: Error, req: RequestWithId, res: express.Response, _next: express.NextFunction) => { logger.error('Unhandled error', { requestId: req.requestId, error: err.message, stack: err.stack, }); res.status(500).json({ error: "Internal Server Error", message: config.nodeEnv === 'development' ? err.message : 'An unexpected error occurred', requestId: req.requestId, }); }); const server = app.listen(port, () => { const baseUrl = `http://localhost:${port}`; logger.info(`F1 MCP Server (HTTP) listening on :${port}`, { port, nodeEnv: config.nodeEnv, apiVersion: config.apiVersion, }); logger.info(`MCP Endpoint: ${baseUrl}/mcp/${config.apiVersion}`); logger.info(`Discovery Endpoint: ${baseUrl}/.well-known/mcp-config`); logger.info(`Health Check: ${baseUrl}/health`); if (config.metricsEnabled) { logger.info(`Metrics: ${baseUrl}/metrics`); } }); // Setup graceful shutdown setupGracefulShutdown(server); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Panth1823/formula1-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server