Skip to content

Creating an MCP Server

This guide walks you through building an MCP server that exposes tools to Mentium AI Agents. We cover Python and TypeScript with complete examples, and link to SDKs for other languages.

Prerequisites

  • Your server must be accessible over HTTPS (required for production)
  • Your server must use the Streamable HTTP transport (not stdio)
  • Your server must accept an API key for authentication (via Authorization: Bearer <key> header)

Install the SDK

bash
pip install "mcp[cli]"
bash
npm install @modelcontextprotocol/sdk zod express
bash
go get github.com/modelcontextprotocol/go-sdk
bash
dotnet add package ModelContextProtocol

Define Tools

Tools are the core of your MCP server. Each tool has a name, description, and input schema. When an AI Agent in Mentium calls a tool, your server executes the function and returns the result.

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My Freight Server")

@mcp.tool()
def quote_get_status(quote_id: str) -> dict:
    """Fetch current quote status and last activity."""
    # Your business logic here — query your database, call an API, etc.
    return {
        "quoteId": quote_id,
        "status": "SENT",
        "opened": True,
        "customerReplied": False,
        "priority": "NORMAL",
    }

@mcp.tool()
def search_carriers(
    origin_zip: str,
    dest_zip: str,
    equipment_type: str = "DRY_VAN",
) -> dict:
    """Search for available carriers on a lane."""
    # Your business logic here
    return {
        "lane": f"{origin_zip} -> {dest_zip}",
        "equipment": equipment_type,
        "carriers": [
            {"name": "Midwest Express", "rate": 2200, "score": 0.92},
            {"name": "Great Lakes Freight", "rate": 2350, "score": 0.87},
        ],
    }
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "My Freight Server",
  version: "1.0.0",
});

server.tool(
  "quote_get_status",
  "Fetch current quote status and last activity",
  { quoteId: z.string() },
  async ({ quoteId }) => {
    // Your business logic here — query your database, call an API, etc.
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            quoteId,
            status: "SENT",
            opened: true,
            customerReplied: false,
            priority: "NORMAL",
          }),
        },
      ],
    };
  }
);

server.tool(
  "search_carriers",
  "Search for available carriers on a lane",
  {
    originZip: z.string(),
    destZip: z.string(),
    equipmentType: z.string().default("DRY_VAN"),
  },
  async ({ originZip, destZip, equipmentType }) => {
    // Your business logic here
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            lane: `${originZip} -> ${destZip}`,
            equipment: equipmentType,
            carriers: [
              { name: "Midwest Express", rate: 2200, score: 0.92 },
              { name: "Great Lakes Freight", rate: 2350, score: 0.87 },
            ],
          }),
        },
      ],
    };
  }
);

TIP

Tool descriptions matter — they help the AI Agent decide when and how to use each tool. Be specific about what the tool does and what it returns.

Define Resources (Optional)

Resources provide read-only context to the AI Agent without side effects. They're useful for giving the agent background information.

python
@mcp.resource("freight://customers/{customer_id}")
def get_customer(customer_id: str) -> str:
    """Customer profile with preferences and history."""
    # Fetch from your database
    customer = db.get_customer(customer_id)
    return json.dumps({
        "name": customer.name,
        "company": customer.company,
        "preferredLanes": customer.lanes,
        "timezone": customer.timezone,
    })

@mcp.resource("freight://quotes/{quote_id}")
def get_quote(quote_id: str) -> str:
    """Full quote details including rate and validity."""
    quote = db.get_quote(quote_id)
    return json.dumps({
        "quoteId": quote.id,
        "equipmentType": quote.equipment,
        "rate": quote.rate,
        "validUntil": quote.valid_until.isoformat(),
    })
typescript
server.resource(
  "freight://customers/{customerId}",
  "Customer profile with preferences and history",
  async (uri) => {
    const customerId = uri.pathname.split("/").pop();
    // Fetch from your database
    const customer = await db.getCustomer(customerId);
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify({
            name: customer.name,
            company: customer.company,
            preferredLanes: customer.lanes,
            timezone: customer.timezone,
          }),
        },
      ],
    };
  }
);

Add Authentication

Your MCP server must validate an API key on every request. Add middleware that checks the Authorization header.

python
import hashlib
from fastapi import Request, HTTPException

# Store hashed keys, not raw keys
VALID_KEY_HASHES = {
    hashlib.sha256(b"your-api-key-here").hexdigest(),
}

async def validate_api_key(request: Request):
    """Middleware to validate API key from Authorization header."""
    auth_header = request.headers.get("Authorization", "")
    api_key = auth_header.removeprefix("Bearer ").strip()

    if not api_key:
        api_key = request.headers.get("X-API-Key", "")

    if not api_key:
        raise HTTPException(status_code=401, detail="Missing API key")

    key_hash = hashlib.sha256(api_key.encode()).hexdigest()
    if key_hash not in VALID_KEY_HASHES:
        raise HTTPException(status_code=401, detail="Invalid API key")
typescript
import crypto from "crypto";

// Store hashed keys, not raw keys
const VALID_KEY_HASHES = new Set([
  crypto.createHash("sha256").update("your-api-key-here").digest("hex"),
]);

function validateApiKey(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const authHeader = req.headers["authorization"] || "";
  let apiKey = authHeader.replace("Bearer ", "");

  if (!apiKey) {
    apiKey = (req.headers["x-api-key"] as string) || "";
  }

  if (!apiKey) {
    return res.status(401).json({ error: "Missing API key" });
  }

  const keyHash = crypto.createHash("sha256").update(apiKey).digest("hex");
  if (!VALID_KEY_HASHES.has(keyHash)) {
    return res.status(401).json({ error: "Invalid API key" });
  }

  next();
}

// Apply to your MCP endpoint
app.use("/mcp", validateApiKey);

WARNING

Never store raw API keys. Always hash them before comparison. See Security for the full security checklist.

Run with Streamable HTTP

Mentium connects to your server over Streamable HTTP. Here's the complete server setup:

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My Freight Server")

# ... define your tools and resources here ...

if __name__ == "__main__":
    # Runs on http://localhost:8000/mcp by default
    mcp.run(transport="streamable-http")
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  StreamableHTTPServerTransport,
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const server = new McpServer({
  name: "My Freight Server",
  version: "1.0.0",
});

// ... define your tools here ...

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

// Apply auth middleware (see above)
app.use("/mcp", validateApiKey);

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // stateless mode
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(8000, () => {
  console.log("MCP server running at http://localhost:8000/mcp");
});

Complete Example

Here's a full, runnable MCP server with tools, authentication, and Streamable HTTP transport:

python
"""
Freight MCP Server — complete example.
Install: pip install "mcp[cli]"
Run:     python server.py
"""
import hashlib
from mcp.server.fastmcp import FastMCP

# API key validation
VALID_KEY_HASHES = {
    hashlib.sha256(b"my-secret-api-key").hexdigest(),
}

mcp = FastMCP("Freight Tools")


@mcp.tool()
def quote_get_status(quote_id: str) -> dict:
    """Fetch current quote status and last activity."""
    return {
        "quoteId": quote_id,
        "status": "SENT",
        "opened": True,
        "customerReplied": False,
    }


@mcp.tool()
def search_carriers(
    origin_zip: str,
    dest_zip: str,
    equipment_type: str = "DRY_VAN",
) -> dict:
    """Search for available carriers on a lane."""
    return {
        "lane": f"{origin_zip} -> {dest_zip}",
        "equipment": equipment_type,
        "carriers": [
            {"name": "Midwest Express", "rate": 2200, "score": 0.92},
            {"name": "Great Lakes Freight", "rate": 2350, "score": 0.87},
        ],
    }


@mcp.tool()
def crm_log_activity(
    customer_id: str,
    activity_type: str,
    summary: str,
    quote_id: str = "",
) -> dict:
    """Record an activity in CRM so humans can see it."""
    return {
        "logged": True,
        "customerId": customer_id,
        "activityType": activity_type,
    }


if __name__ == "__main__":
    mcp.run(transport="streamable-http")
typescript
/**
 * Freight MCP Server — complete example.
 * Install: npm install @modelcontextprotocol/sdk zod express
 * Run:     npx tsx server.ts
 */
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  StreamableHTTPServerTransport,
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import crypto from "crypto";
import { z } from "zod";

// API key validation
const VALID_KEY_HASHES = new Set([
  crypto.createHash("sha256").update("my-secret-api-key").digest("hex"),
]);

function validateApiKey(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const apiKey = (req.headers["authorization"] || "")
    .replace("Bearer ", "");
  if (!apiKey) return res.status(401).json({ error: "Missing API key" });

  const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
  if (!VALID_KEY_HASHES.has(hash))
    return res.status(401).json({ error: "Invalid API key" });

  next();
}

const server = new McpServer({
  name: "Freight Tools",
  version: "1.0.0",
});

server.tool(
  "quote_get_status",
  "Fetch current quote status and last activity",
  { quoteId: z.string() },
  async ({ quoteId }) => ({
    content: [
      {
        type: "text",
        text: JSON.stringify({
          quoteId,
          status: "SENT",
          opened: true,
          customerReplied: false,
        }),
      },
    ],
  })
);

server.tool(
  "search_carriers",
  "Search for available carriers on a lane",
  {
    originZip: z.string(),
    destZip: z.string(),
    equipmentType: z.string().default("DRY_VAN"),
  },
  async ({ originZip, destZip, equipmentType }) => ({
    content: [
      {
        type: "text",
        text: JSON.stringify({
          lane: `${originZip} -> ${destZip}`,
          equipment: equipmentType,
          carriers: [
            { name: "Midwest Express", rate: 2200, score: 0.92 },
            { name: "Great Lakes Freight", rate: 2350, score: 0.87 },
          ],
        }),
      },
    ],
  })
);

server.tool(
  "crm_log_activity",
  "Record an activity in CRM so humans can see it",
  {
    customerId: z.string(),
    activityType: z.string(),
    summary: z.string(),
    quoteId: z.string().optional(),
  },
  async ({ customerId, activityType }) => ({
    content: [
      {
        type: "text",
        text: JSON.stringify({
          logged: true,
          customerId,
          activityType,
        }),
      },
    ],
  })
);

const app = express();
app.use(express.json());
app.use("/mcp", validateApiKey);

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(8000, () => {
  console.log("MCP server running at http://localhost:8000/mcp");
});

Test Your Server

Use the official MCP Inspector to test your server before connecting it to Mentium:

bash
npx -y @modelcontextprotocol/inspector

Then connect to http://localhost:8000/mcp in the inspector UI. You should see your tools listed and be able to call them interactively.

Deploy

Before connecting to Mentium, your server needs to be accessible over HTTPS:

  • Cloud VM (EC2, GCP, DigitalOcean) — run behind a reverse proxy (nginx, Caddy) with TLS
  • Container (ECS, Cloud Run, Fly.io) — most platforms handle TLS automatically
  • Serverless (Lambda, Cloud Functions) — wrap your MCP handler in the platform's HTTP handler

WARNING

Never expose your MCP server over plain HTTP in production. Mentium requires HTTPS for all external MCP server connections.

Connect to Mentium

Once deployed, add your server in Settings > MCP Servers:

  1. Enter your server's HTTPS URL (e.g., https://mcp.yourcompany.com/mcp)
  2. Enter the API key you configured
  3. Click Connect & Discover Tools — Mentium will list all available tools
  4. Enable the tools you want AI Agents to use

SDKs for Other Languages

MCP has official and community SDKs for many languages:

LanguagePackageLinks
PythonmcpGitHub · PyPI · Docs
TypeScript@modelcontextprotocol/sdkGitHub · npm · Docs
Gogithub.com/modelcontextprotocol/go-sdkGitHub · pkg.go.dev
C#ModelContextProtocolGitHub · NuGet
Javaio.modelcontextprotocol.sdk:mcpGitHub · Maven
Kotlinkotlin-sdkGitHub
RustrmcpGitHub · crates.io
Swiftswift-sdkGitHub
Rubyruby-sdkGitHub

INFO

All SDKs support the same MCP protocol. As long as your server implements Streamable HTTP transport and API key authentication, it will work with Mentium regardless of the language.

For the full protocol specification, see modelcontextprotocol.io.

Mentium TMS API Documentation