Templatetypescriptexpressbeginner✓ Production ReadyFree✅ Code Verified

TypeScript MCP Server — Railway Deployment

Production-ready TypeScript MCP server with HTTP transport, API key auth, structured logging, health monitoring, and one-click Railway deployment. Includes 3 example tools and full test suite.

📖 5 min⚙️ Setup: 10 minutilities
typescriptmcprailwayexpresshttp-transportapi-key-authpinozoddocker

Files(20)

import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { config } from "./config";
import { logger } from "./logger";
import { apiKeyAuth } from "./auth";
import { healthHandler } from "./health";
import { registerAnalyzeText } from "./tools/analyzeText";
import { registerFetchUrlMetadata } from "./tools/fetchUrlMetadata";
import { registerMarkdownToText } from "./tools/markdownToText";

const app = express();
app.use(express.json());

app.get("/health", healthHandler);

function createMcpServer(): McpServer {
  const server = new McpServer({
    name: "mcpforge-demo-server",
    version: "1.0.0",
  });

  registerAnalyzeText(server);
  registerFetchUrlMetadata(server);
  registerMarkdownToText(server);

  return server;
}

app.all("/mcp", apiKeyAuth, async (req, res) => {
  // MCP Streamable HTTP transport requires Accept: application/json, text/event-stream.
  // Set it if absent so non-compliant clients still work.
  if (!req.headers["accept"]?.includes("text/event-stream")) {
    req.headers["accept"] = "application/json, text/event-stream";
  }

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  const server = createMcpServer();

  res.on("close", () => {
    transport.close().catch((err: unknown) => logger.error(err, "Error closing transport"));
    server.close().catch((err: unknown) => logger.error(err, "Error closing server"));
  });

  try {
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (err) {
    logger.error(err, "MCP request failed");
    if (!res.headersSent) {
      res.status(500).json({ error: "Internal server error" });
    }
  }
});

const httpServer = app.listen(config.PORT, () => {
  logger.info(
    { port: config.PORT, env: config.NODE_ENV },
    "MCPForge demo server started"
  );
});

process.on("SIGTERM", () => {
  logger.info("SIGTERM received — shutting down");
  httpServer.close(() => {
    logger.info("HTTP server closed");
    process.exit(0);
  });
});

export { app };

Interactive tester

Test Your Server

Enter a deployed HTTP MCP server URL and API key to run live browser checks.

No signup

Use the public MCP endpoint, not a dashboard URL or local stdio command.

The key stays in this browser check and is sent to the endpoint you enter.

For local servers, use the local testing guide or MCP Inspector. This online tester works best with deployed HTTP MCP servers.

TypeScript MCP Server — Railway Deployment

A production-ready TypeScript MCP server template built with Express and the official MCP SDK. Ships with API key authentication, structured logging, health monitoring, and Railway deployment config.

What's Included

  • MCP Streamable HTTP transport via @modelcontextprotocol/sdk (stateless mode)
  • API key authentication — checks X-API-Key header
  • Environment validation with Zod — exits on bad config
  • Pino structured logging — pretty in dev, JSON in production
  • Health endpoint at GET /health
  • 3 example MCP tools: analyzeText, fetchUrlMetadata, markdownToText
  • Multi-stage Dockerfile — production image omits dev deps
  • Docker Compose with health check
  • Railway deployment ready — set MCP_API_KEY and push

Project Structure

├── src/
│   ├── tools/
│   │   ├── analyzeText.ts
│   │   ├── fetchUrlMetadata.ts
│   │   └── markdownToText.ts
│   ├── auth.ts
│   ├── config.ts
│   ├── health.ts
│   ├── logger.ts
│   └── server.ts
├── tests/
│   ├── analyzeText.test.ts
│   ├── auth.test.ts
│   ├── health.test.ts
│   └── markdownToText.test.ts
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── jest.config.ts
├── package.json
├── tsconfig.json
└── tsconfig.test.json

Quick Start

cp .env.example .env
# Set MCP_API_KEY to a strong secret (min 16 chars)
npm install
npm run dev

Server starts at http://localhost:3000/mcp.

Deploy to Railway

  1. Push repo to GitHub
  2. New Railway project → Deploy from GitHub repo
  3. Set MCP_API_KEY in Railway Variables
  4. Railway detects Dockerfile and deploys automatically

Your endpoint: https://<project>.up.railway.app/mcp

Authentication

X-API-Key: your-api-key-here

Requests without a valid key return 401 Unauthorized.

Configuration

VariableRequiredDefaultDescription
MCP_API_KEYAPI key, min 16 chars
PORT3000Server port
NODE_ENVdevelopmentEnvironment
LOG_LEVELinfoPino log level
REQUEST_TIMEOUT_MS30000Outbound HTTP timeout

Adding a New Tool

  1. Create src/tools/myTool.ts
  2. Export your tool function with Zod schema
  3. Register in src/server.ts

Testing

npm test
npm run test:coverage

Production Checklist

  • MCP_API_KEY is cryptographically random (min 32 chars)
  • NODE_ENV=production set in deployment
  • Server behind TLS-terminating proxy
  • /health wired to load balancer health checks
  • Docker image built from runner stage

✅ Template ready. What's next?

  1. 1.Copy or download the template above
  2. 2.Deploy your MCP server (Railway, Vercel)
  3. 3.Test your deployment
  4. 4.Verify production readiness