Skip to main content
Glama
index.ts14.6 kB
import express, { Request, Response } from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { GraphQLClient, gql } from "graphql-request"; import { randomUUID } from "crypto"; import { z } from "zod"; const app = express(); const PORT = process.env.PORT || 3000; const RAILWAY_API_TOKEN = process.env.RAILWAY_API_TOKEN || ""; const client = new GraphQLClient("https://backboard.railway.app/graphql/v2", { headers: { Authorization: `Bearer ${RAILWAY_API_TOKEN}` }, }); // Store transports by session ID const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {}; // GraphQL queries const queries = { listProjects: gql`query { projects { edges { node { id name description createdAt updatedAt } } } }`, getProject: gql`query($id: String!) { project(id: $id) { id name description createdAt services { edges { node { id name } } } environments { edges { node { id name } } } } }`, listServices: gql`query($projectId: String!) { project(id: $projectId) { services { edges { node { id name icon createdAt } } } } }`, listDeployments: gql`query($projectId: String!, $serviceId: String!) { deployments(projectId: $projectId, serviceId: $serviceId, first: 10) { edges { node { id status createdAt } } } }`, getDeploymentLogs: gql`query($deploymentId: String!, $limit: Int) { deploymentLogs(deploymentId: $deploymentId, limit: $limit) { message timestamp severity } }`, listVariables: gql`query($projectId: String!, $environmentId: String!, $serviceId: String!) { variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) { name value } }`, listEnvironments: gql`query($projectId: String!) { project(id: $projectId) { environments { edges { node { id name } } } } }`, createProject: gql`mutation($name: String!, $description: String) { projectCreate(input: { name: $name, description: $description }) { id name } }`, createService: gql`mutation($projectId: String!, $name: String!) { serviceCreate(input: { projectId: $projectId, name: $name }) { id name } }`, deployFromGithub: gql`mutation($projectId: String!, $repo: String!, $branch: String) { serviceCreate(input: { projectId: $projectId, source: { repo: $repo, branch: $branch } }) { id name } }`, createDomain: gql`mutation($serviceId: String!, $environmentId: String!) { serviceDomainCreate(input: { serviceId: $serviceId, environmentId: $environmentId }) { domain } }`, setVariable: gql`mutation($projectId: String!, $environmentId: String!, $serviceId: String!, $name: String!, $value: String!) { variableUpsert(input: { projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId, name: $name, value: $value }) }`, restartService: gql`mutation($serviceId: String!, $environmentId: String!) { serviceInstanceRedeploy(serviceId: $serviceId, environmentId: $environmentId) }`, deleteProject: gql`mutation($id: String!) { projectDelete(id: $id) }`, deleteService: gql`mutation($id: String!) { serviceDelete(id: $id) }`, deleteVariable: gql`mutation($projectId: String!, $environmentId: String!, $serviceId: String!, $name: String!) { variableDelete(input: { projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId, name: $name }) }`, }; // Create and configure MCP server function createMcpServer(): McpServer { const server = new McpServer({ name: "railway-mcp-server", version: "2.0.0", }); // Register tools server.tool("list_projects", "List all Railway projects", {}, async () => { const data: any = await client.request(queries.listProjects); return { content: [{ type: "text", text: JSON.stringify(data.projects.edges.map((e: any) => e.node), null, 2) }] }; }); server.tool("get_project", "Get project details", { projectId: z.string().describe("Project ID") }, async ({ projectId }) => { const data: any = await client.request(queries.getProject, { id: projectId }); return { content: [{ type: "text", text: JSON.stringify(data.project, null, 2) }] }; }); server.tool("list_services", "List services in a project", { projectId: z.string().describe("Project ID") }, async ({ projectId }) => { const data: any = await client.request(queries.listServices, { projectId }); return { content: [{ type: "text", text: JSON.stringify(data.project.services.edges.map((e: any) => e.node), null, 2) }] }; }); server.tool("list_deployments", "List deployments for a service", { projectId: z.string().describe("Project ID"), serviceId: z.string().describe("Service ID") }, async ({ projectId, serviceId }) => { const data: any = await client.request(queries.listDeployments, { projectId, serviceId }); return { content: [{ type: "text", text: JSON.stringify(data.deployments.edges.map((e: any) => e.node), null, 2) }] }; }); server.tool("get_deployment_logs", "Get deployment logs", { deploymentId: z.string().describe("Deployment ID"), limit: z.number().optional().describe("Number of log lines") }, async ({ deploymentId, limit }) => { const data: any = await client.request(queries.getDeploymentLogs, { deploymentId, limit: limit || 100 }); return { content: [{ type: "text", text: JSON.stringify(data.deploymentLogs, null, 2) }] }; }); server.tool("list_variables", "List environment variables", { projectId: z.string(), environmentId: z.string(), serviceId: z.string() }, async (args) => { const data: any = await client.request(queries.listVariables, args); return { content: [{ type: "text", text: JSON.stringify(data.variables, null, 2) }] }; }); server.tool("list_environments", "List environments in a project", { projectId: z.string() }, async ({ projectId }) => { const data: any = await client.request(queries.listEnvironments, { projectId }); return { content: [{ type: "text", text: JSON.stringify(data.project.environments.edges.map((e: any) => e.node), null, 2) }] }; }); server.tool("create_project", "Create a new Railway project", { name: z.string().describe("Project name"), description: z.string().optional().describe("Project description") }, async (args) => { const data: any = await client.request(queries.createProject, args); return { content: [{ type: "text", text: JSON.stringify(data.projectCreate, null, 2) }] }; }); server.tool("create_service", "Create a new service in a project", { projectId: z.string(), name: z.string() }, async (args) => { const data: any = await client.request(queries.createService, args); return { content: [{ type: "text", text: JSON.stringify(data.serviceCreate, null, 2) }] }; }); server.tool("deploy_from_github", "Deploy a service from a GitHub repo", { projectId: z.string(), repo: z.string().describe("GitHub repo (user/repo)"), branch: z.string().optional() }, async (args) => { const data: any = await client.request(queries.deployFromGithub, args); return { content: [{ type: "text", text: JSON.stringify(data.serviceCreate, null, 2) }] }; }); server.tool("create_domain", "Generate a domain for a service", { serviceId: z.string(), environmentId: z.string() }, async (args) => { const data: any = await client.request(queries.createDomain, args); return { content: [{ type: "text", text: JSON.stringify(data.serviceDomainCreate, null, 2) }] }; }); server.tool("set_variable", "Set an environment variable", { projectId: z.string(), environmentId: z.string(), serviceId: z.string(), name: z.string(), value: z.string() }, async (args) => { await client.request(queries.setVariable, args); return { content: [{ type: "text", text: JSON.stringify({ success: true, name: args.name }, null, 2) }] }; }); server.tool("set_variables_bulk", "Set multiple environment variables at once", { projectId: z.string(), environmentId: z.string(), serviceId: z.string(), variables: z.record(z.string()).describe("Key-value pairs") }, async ({ projectId, environmentId, serviceId, variables }) => { const results = []; for (const [name, value] of Object.entries(variables)) { await client.request(queries.setVariable, { projectId, environmentId, serviceId, name, value }); results.push({ name, success: true }); } return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; }); server.tool("restart_service", "Restart a service (redeploy)", { serviceId: z.string(), environmentId: z.string() }, async (args) => { await client.request(queries.restartService, args); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Service restarting" }, null, 2) }] }; }); server.tool("delete_project", "Delete a project", { projectId: z.string() }, async ({ projectId }) => { await client.request(queries.deleteProject, { id: projectId }); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Project deleted" }, null, 2) }] }; }); server.tool("delete_service", "Delete a service", { serviceId: z.string() }, async ({ serviceId }) => { await client.request(queries.deleteService, { id: serviceId }); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Service deleted" }, null, 2) }] }; }); server.tool("delete_variable", "Delete an environment variable", { projectId: z.string(), environmentId: z.string(), serviceId: z.string(), name: z.string() }, async (args) => { await client.request(queries.deleteVariable, args); return { content: [{ type: "text", text: JSON.stringify({ success: true, name: args.name }, null, 2) }] }; }); return server; } // CORS middleware app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept"); res.header("Access-Control-Expose-Headers", "Mcp-Session-Id"); if (req.method === "OPTIONS") { return res.status(200).end(); } next(); }); app.use(express.json()); // Modern Streamable HTTP transport - POST /mcp app.post("/mcp", async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string; const isInitRequest = req.body?.method === "initialize"; try { if (isInitRequest) { const newSessionId = randomUUID(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId, }); transports[newSessionId] = transport; const mcpServer = createMcpServer(); await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); } else if (sessionId && transports[sessionId]) { const transport = transports[sessionId] as StreamableHTTPServerTransport; await transport.handleRequest(req, res, req.body); } else { res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or missing session" }, id: req.body?.id || null }); } } catch (error) { console.error("MCP POST error:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: req.body?.id || null }); } } }); // Session cleanup app.delete("/mcp", async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string; if (sessionId && transports[sessionId]) { const transport = transports[sessionId]; await transport.close?.(); delete transports[sessionId]; res.status(204).end(); } else { res.status(404).json({ error: "Session not found" }); } }); // Legacy SSE transport - GET /sse app.get("/sse", async (req: Request, res: Response) => { console.log("SSE connection request received"); res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); const transport = new SSEServerTransport("/messages", res); const sessionId = transport.sessionId; transports[sessionId] = transport; console.log(`SSE session created: ${sessionId}`); const mcpServer = createMcpServer(); transport.onclose = () => { console.log(`SSE session closed: ${sessionId}`); delete transports[sessionId]; }; try { await mcpServer.connect(transport); console.log(`MCP server connected to SSE session: ${sessionId}`); } catch (error) { console.error("SSE connection error:", error); delete transports[sessionId]; if (!res.headersSent) { res.status(500).end(); } } }); // Legacy SSE transport - POST /messages app.post("/messages", async (req: Request, res: Response) => { const sessionId = req.query.sessionId as string; console.log(`Message received for session: ${sessionId}`); if (!sessionId) { return res.status(400).json({ error: "Missing sessionId" }); } const transport = transports[sessionId]; if (!transport || !(transport instanceof SSEServerTransport)) { return res.status(404).json({ error: "Session not found" }); } try { await transport.handlePostMessage(req, res, req.body); } catch (error) { console.error("Message handling error:", error); if (!res.headersSent) { res.status(500).json({ error: "Internal server error" }); } } }); // Health check app.get("/health", (req, res) => { res.json({ status: "ok", sessions: Object.keys(transports).length, version: "2.0.0" }); }); // Root endpoint app.get("/", (req, res) => { res.json({ name: "Railway MCP Server", version: "2.0.0", endpoints: { streamableHttp: "/mcp", sse: "/sse", messages: "/messages", health: "/health" }, tools: [ "list_projects", "get_project", "list_services", "list_deployments", "get_deployment_logs", "list_variables", "list_environments", "create_project", "create_service", "deploy_from_github", "create_domain", "set_variable", "set_variables_bulk", "restart_service", "delete_project", "delete_service", "delete_variable" ] }); }); app.listen(PORT, () => { console.log(`Railway MCP Server running on port ${PORT}`); });

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/hemichaeli/railway-mcp-server-v2'

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