@rfdez/pvpc-mcp-server
Version:
Retrieve daily PVPC electricity tariffs for 2.0 TD consumers, published by Red Eléctrica.
164 lines (163 loc) • 5.54 kB
JavaScript
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { Command, Option } from "commander";
import cors from "cors";
import express, {} from "express";
import helmet from "helmet";
import { PvpcMcpServer } from "./mcp.js";
import { PvpcApiClient } from "./pvpc.js";
import { extractBearerToken, extractHeaderValue } from "./utils.js";
const program = new Command()
.addOption(new Option("--transport <type>", "transport type")
.choices(["stdio", "http"])
.default("stdio"))
.addOption(new Option("--port <number>", "port for HTTP transport")
.implies({ transport: "http" })
.env("PORT")
.default("8080"))
.addOption(new Option("--api-key <key>", "ESIOS API key for authentication")
.implies({ transport: "stdio" })
.conflicts("port"))
.allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
.parse(process.argv);
const options = program.opts();
async function main() {
if (options.transport === "http") {
const defaultPort = 8080;
const port = parseInt(options.port, 10);
await runHttpServer(isNaN(port) ? defaultPort : port);
}
else {
await runStdioServer(options.apiKey);
}
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
process.exit(1);
});
async function runHttpServer(port) {
const app = express();
app.use(express.json());
app.use(helmet());
app.use(cors({
origin: "*",
methods: ["GET", "POST", "DELETE"],
exposedHeaders: ["Mcp-Session-Id"],
allowedHeaders: [
"Content-Type",
"mcp-session-id",
"mcp-protocol-version",
],
}));
app.get("/ping", (_req, res) => {
res.status(200).send("pong");
});
app.post("/mcp", async (req, res) => {
// Check headers in order of preference
const apiKey = extractBearerToken(req.headers.authorization) ||
extractHeaderValue(req.headers["Esios-API-Key"]) ||
extractHeaderValue(req.headers["X-API-Key"]) ||
extractHeaderValue(req.headers["esios-api-key"]) ||
extractHeaderValue(req.headers["x-api-key"]) ||
extractHeaderValue(req.headers["Esios_API_Key"]) ||
extractHeaderValue(req.headers["X_API_Key"]) ||
extractHeaderValue(req.headers["esios_api_key"]) ||
extractHeaderValue(req.headers["x_api_key"]);
const apiClient = new PvpcApiClient(apiKey);
const mcpServer = new PvpcMcpServer(apiClient);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
res.on("close", async () => {
await transport.close();
await mcpServer.stop();
});
await mcpServer.start(transport);
await transport.handleRequest(req, res, req.body);
}
catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
id: null,
error: {
code: -32603,
message: "Internal server error.",
},
});
}
}
});
app.get("/mcp", (_req, res) => {
res.status(405).json({
jsonrpc: "2.0",
id: null,
error: {
code: -32000,
message: "Method not allowed.",
},
});
});
app.delete("/mcp", (_req, res) => {
res.status(405).json({
jsonrpc: "2.0",
id: null,
error: {
code: -32000,
message: "Method not allowed.",
},
});
});
const httpServer = app.listen(port, (error) => {
if (error) {
console.error("Error starting HTTP server:", error);
process.exit(1);
}
console.log(`Server is running on http://localhost:${port}/mcp`);
});
process.on("SIGINT", () => {
httpServer.close();
process.exit(0);
});
process.on("SIGTERM", () => {
httpServer.close((err) => {
if (err) {
console.error("Error closing HTTP server:", err);
process.exit(1);
}
process.exit(0);
});
});
}
async function runStdioServer(apiKey) {
const apiClient = new PvpcApiClient(apiKey);
const mcpServer = new PvpcMcpServer(apiClient);
const transport = new StdioServerTransport();
await mcpServer.start(transport);
console.error("Server running on stdio");
process.on("SIGINT", async () => {
await mcpServer.stop();
process.exit(0);
});
process.on("SIGTERM", async () => {
try {
await mcpServer.stop();
}
catch (error) {
console.error("Error stopping server:", error);
process.exit(1);
}
process.exit(0);
});
}