Post

Meow's AIML - MCP Deployment

MCP Deployment

ref:


Overview 概述

MCP (Model Context Protocol)

is an open standard that lets AI models communicate with external tools, data sources, and services in a structured, secure way. Building an MCP server allows you to expose resources (readable data) and tools (callable actions) to any compliant LLM client — Claude, Cursor, Cline, or a custom agent.

This note covers the full lifecycle from local development to production deployment:

  • Setting up a Python MCP server project (traditional SDK or FastMCP)
  • Defining resources, tools, and prompts
  • Debugging with MCP Inspector
  • Writing MCP clients and connecting real LLMs
  • Deploying via Docker, AWS Lambda/ECS, and Aliyun Serverless (FC 3.0)

Prerequisites 准备工作

RequirementNotes
Python 3.8+3.10+ recommended
MCP Python SDKpip install mcp or pip install fastmcp
MCP Inspectornpx @modelcontextprotocol/inspector
GitVersion control
DockerFor containerized deployment
Node.js 16+For Inspector and some clients

Core Concepts 核心概念

An MCP system has three roles:

RoleResponsibility
ServerExposes resources and tools to LLMs
ClientConnects an LLM to one or more servers
ProtocolGoverns message framing between client and server

Three primitive types that a server can expose:

  • **Resource**

    — Static or dynamic data the LLM can read (files, DB rows, API results)

  • **Tool**

    — Callable functions the LLM can invoke to take actions

  • **Prompt**

    — Reusable instruction templates the LLM can request

Communication flow:

1
2
3
4
LLM (via Client)
  → request resource or call tool
  → Server processes and returns standardized response
  → LLM uses result in its reasoning

Two transport modes:

TransportUse case
stdioLocal tools, CLI, desktop apps — subprocess communication
SSE (HTTP)Remote servers, web services, multi-user environments

Project Setup 项目搭建

1
2
3
4
5
6
7
8
9
10
11
mkdir my_mcp_server
cd my_mcp_server

# Create virtual environment (use pyenv)
pyenv virtualenv 3.11.4 mcp-server-env
pyenv local mcp-server-env

# Project structure
mkdir -p src/resources src/tools tests
touch src/__init__.py src/resources/__init__.py src/tools/__init__.py
touch requirements.txt README.md

requirements.txt:

1
2
3
4
5
mcp>=1.0.0
fastmcp>=0.1.0
pydantic>=2.0.0
pytest>=7.0.0
httpx>=0.27.0
1
pip install -r requirements.txt

FastMCP Quick Start 快速开始

FastMCP

is the high-level wrapper over the official MCP SDK. It uses decorators (@mcp.tool(), @mcp.resource(), @mcp.prompt()) to define server capabilities with minimal boilerplate.

Define a Tool — @mcp.tool()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("DemoServer")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.tool()
def get_weather(city: str) -> str:
    """Get weather for a city."""
    # In production: call a real weather API
    return f"The weather in {city} is sunny, 25°C."

Type hints are used automatically to build the JSON schema the client sends to the LLM.

Define a Resource — @mcp.resource()

1
2
3
4
5
6
7
8
9
@mcp.resource("config://app")
def get_app_config() -> str:
    """Return application config."""
    return "debug=true\nversion=1.0"

@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
    """Return a user profile by ID."""
    return f"User {user_id}: Engineer, 5yr exp"

Resources are read-only — they never trigger side effects.

Define a Prompt — @mcp.prompt()

1
2
3
4
@mcp.prompt()
def review_code(code: str) -> str:
    """Generate a code review prompt."""
    return f"Please review the following code and identify bugs:\n\n{code}"

Run the Server

1
2
3
4
5
# stdio mode (default)
mcp run server.py

# Or directly
python server.py
1
2
3
if __name__ == "__main__":
    mcp.run()                        # stdio
    # mcp.run(transport="sse")       # SSE/HTTP

Traditional SDK: Resources and Tools

When using the lower-level mcp SDK directly (not FastMCP), define resources and tools explicitly.

Defining Resources

src/resources/user_profiles.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import List, Dict, Any
from pydantic import BaseModel
import logging

logger = logging.getLogger("mcp_server.resources")

class UserProfile(BaseModel):
    name: str
    role: str
    department: str = "General"
    years_experience: int = 0

def fetch_user_profiles() -> List[Dict[str, Any]]:
    users = [
        UserProfile(name="Alice", role="Engineer", department="Engineering", years_experience=5),
        UserProfile(name="Bob", role="Product Manager", department="Product", years_experience=3),
    ]
    logger.info(f"Fetched {len(users)} user profiles")
    return [u.model_dump() for u in users]

src/server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from mcp_server import MCPServer, Resource
import logging
from src.resources.user_profiles import fetch_user_profiles

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp_server")

def main() -> None:
    server = MCPServer(
        name="MyMCPServer",
        version="0.1.0",
        description="A simple MCP server example"
    )

    user_profiles = Resource(
        name="user_profiles",
        description="List of user profiles from the company database.",
        fetch_fn=fetch_user_profiles
    )
    server.add_resource(user_profiles)

    logger.info("Starting MCP server...")
    server.start()

if __name__ == "__main__":
    main()

Defining Tools

src/tools/user_management.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field, ValidationError
import logging

logger = logging.getLogger("mcp_server.tools")

class CreateUserRequest(BaseModel):
    name: str = Field(..., min_length=2)
    role: str = Field(..., min_length=2)
    department: Optional[str] = Field("General")
    years_experience: Optional[int] = Field(0, ge=0)

def create_user_profile(request_data: Dict[str, Any]) -> Dict[str, Any]:
    try:
        user_data = CreateUserRequest(**request_data)
        logger.info(f"Creating user: {user_data.name}")
        return {
            "status": "success",
            "message": f"User {user_data.name} created successfully",
            "user": user_data.model_dump()
        }
    except ValidationError as e:
        logger.error(f"Validation error: {e}")
        return {"status": "error", "message": "Invalid user data", "details": str(e)}
    except Exception as e:
        logger.error(f"Error creating user: {e}")
        return {"status": "error", "message": "Failed to create user", "details": str(e)}

Register the tool in server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from mcp_server import MCPServer, Resource, Tool
from src.tools.user_management import create_user_profile

# Inside main():
create_user = Tool(
    name="create_user_profile",
    description="Create a new user profile in the database.",
    parameters={
        "name": {"type": "string", "description": "User's full name"},
        "role": {"type": "string", "description": "User's job role"},
        "department": {"type": "string", "description": "User's department (optional)"},
        "years_experience": {"type": "integer", "description": "Years of experience (optional)"}
    },
    execute_fn=create_user_profile
)
server.add_tool(create_user)

Error Handling and Validation

Centralize validation using Pydantic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# src/utils/validation.py
from typing import Dict, Any, Optional, Type, Tuple
from pydantic import BaseModel, ValidationError
import logging

logger = logging.getLogger("mcp_server.validation")

def validate_request(
    data: Dict[str, Any],
    model_class: Type[BaseModel]
) -> Tuple[Optional[BaseModel], Optional[Dict[str, Any]]]:
    try:
        return model_class(**data), None
    except ValidationError as e:
        error_dict = {
            "status": "error",
            "message": "Validation failed",
            "errors": e.errors()
        }
        logger.error(f"Validation error: {e.errors()}")
        return None, error_dict

Debugging with MCP Inspector 调试工具

MCP Inspector

is an interactive browser-based tool for testing MCP servers without a full LLM client.

1
2
3
4
5
6
7
8
9
10
# Method 1: npx (no install required)
npx @modelcontextprotocol/inspector python server.py

# Method 2: via mcp CLI
mcp dev server.py

# Method 3: with environment variables
npx @modelcontextprotocol/inspector \
  -e API_KEY=your_key \
  python server.py

After running, open http://localhost:5173 in a browser. You will see:

  • Resources tab — list and read all registered resources
  • Tools tab — invoke tools with custom parameters
  • Prompts tab — preview prompt templates
  • Console — view all JSON-RPC messages in real time

Typical debug workflow:

  1. Start the Inspector pointing at your server
  2. Click a tool → fill parameters → click “Run”
  3. Inspect the JSON request/response in the Console tab
  4. Confirm the output matches expectations before connecting to a real client

Client Development 客户端开发

stdio Client

The stdio client launches the MCP server as a subprocess and communicates over stdin/stdout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # List available tools
            tools = await session.list_tools()
            print("Tools:", [t.name for t in tools.tools])

            # List resources
            resources = await session.list_resources()
            print("Resources:", [r.name for r in resources.resources])

            # Call a tool
            result = await session.call_tool("add", {"a": 3, "b": 5})
            print("3 + 5 =", result.content[0].text)

asyncio.run(main())

SSE Client

For servers running over HTTP/SSE:

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client

async def main():
    async with sse_client("http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("get_weather", {"city": "Beijing"})
            print(result.content[0].text)

asyncio.run(main())

Client with DeepSeek LLM

Full agentic loop where the LLM decides which tools to call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import asyncio
import json
from openai import AsyncOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

client = AsyncOpenAI(
    api_key="your_deepseek_api_key",
    base_url="https://api.deepseek.com",
)

async def run_agent():
    server_params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Get tool list and convert to OpenAI format
            tools_response = await session.list_tools()
            tools = [
                {
                    "type": "function",
                    "function": {
                        "name": t.name,
                        "description": t.description,
                        "parameters": t.inputSchema,
                    }
                }
                for t in tools_response.tools
            ]

            messages = [
                {"role": "user", "content": "What is the weather in Shanghai?"}
            ]

            # LLM decides to call a tool
            response = await client.chat.completions.create(
                model="deepseek-chat",
                messages=messages,
                tools=tools,
            )

            # Execute tool calls returned by the LLM
            assistant_msg = response.choices[0].message
            messages.append(assistant_msg)

            for tool_call in (assistant_msg.tool_calls or []):
                args = json.loads(tool_call.function.arguments)
                result = await session.call_tool(tool_call.function.name, args)
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result.content[0].text,
                })

            # Final LLM response after tool results
            final = await client.chat.completions.create(
                model="deepseek-chat",
                messages=messages,
            )
            print(final.choices[0].message.content)

asyncio.run(run_agent())

Claude Desktop Configuration

To register an MCP server with Claude Desktop, edit ~/Library/Application Support/Claude/claude_desktop_config.json:

1
2
3
4
5
6
7
8
9
10
11
{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"],
      "env": {
        "API_KEY": "your_api_key"
      }
    }
  }
}

After saving, restart Claude Desktop. The registered server’s tools and resources appear automatically in every conversation.

For a server installed as a package:

1
2
3
4
5
6
7
8
{
  "mcpServers": {
    "weather": {
      "command": "uvx",
      "args": ["mcp-server-weather"]
    }
  }
}

Cline and Cursor Configuration

Cline (VS Code extension) and Cursor both support MCP servers via a JSON config file.

Cline — ~/.cline/mcp_settings.json:

1
2
3
4
5
6
7
8
9
10
11
{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["/path/to/server.py"],
      "env": {
        "API_KEY": "your_key"
      }
    }
  }
}

Cursor — project .cursor/mcp.json:

1
2
3
4
5
6
7
8
{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["server.py"]
    }
  }
}

Sampling Callback 采样回调

Sampling

is an MCP feature that lets the server request the LLM to generate text — the reverse of the normal flow. This enables pre/post tool hooks and multi-step reasoning patterns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.types import CreateMessageRequest, CreateMessageResult, TextContent

async def handle_sampling(request: CreateMessageRequest) -> CreateMessageResult:
    """Called when the server wants the LLM to generate text."""
    print(f"[Sampling] Server requests generation: {request.messages[-1].content.text}")
    # In production: forward to a real LLM
    return CreateMessageResult(
        role="assistant",
        content=TextContent(type="text", text="Sampling response placeholder"),
        model="mock-model",
        stopReason="endTurn",
    )

async def main():
    server_params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write,
            sampling_callback=handle_sampling
        ) as session:
            await session.initialize()
            result = await session.call_tool("tool_that_uses_sampling", {})
            print(result)

asyncio.run(main())

The sampling_callback is invoked whenever the server calls create_message. Use it to:

  • Log every LLM interaction for auditing
  • Route to different models per request
  • Add safety filters before responses reach the server

Advanced Features 高级特性

Lifecycle Management 生命周期管理

Use the lifespan context manager to initialize and clean up shared resources (DB connections, HTTP clients, caches) that tools depend on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
import httpx

@asynccontextmanager
async def lifespan(app):
    # Startup: initialize shared state
    app.state.http_client = httpx.AsyncClient()
    app.state.db_pool = await create_db_pool()
    print("Server started, resources initialized.")
    yield
    # Shutdown: clean up
    await app.state.http_client.aclose()
    await app.state.db_pool.close()
    print("Server shutting down, resources released.")

mcp = FastMCP("MyServer", lifespan=lifespan)

@mcp.tool()
async def fetch_data(url: str) -> str:
    # Access the shared client from app state
    response = await mcp.app.state.http_client.get(url)
    return response.text

Prompt Primitives

Prompts are reusable instruction templates. They appear in MCP-aware editors as slash commands or context actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from mcp.server.fastmcp import FastMCP
from mcp.types import GetPromptResult, PromptMessage, TextContent

mcp = FastMCP("PromptServer")

@mcp.prompt()
def debug_error(error: str, language: str = "Python") -> GetPromptResult:
    return GetPromptResult(
        description=f"Debug a {language} error",
        messages=[
            PromptMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"You are an expert {language} developer. Debug this error:\n\n{error}"
                )
            )
        ]
    )

Resource Primitives

Resources support URI templates for dynamic data:

1
2
3
4
5
6
7
8
9
10
@mcp.resource("file://{path}")
async def read_file(path: str) -> str:
    with open(path, "r") as f:
        return f.read()

@mcp.resource("db://users/{user_id}")
async def get_user(user_id: str) -> str:
    # Query database
    user = await db.get_user(user_id)
    return user.to_json()

Resources can also return binary data (images, PDFs) using BlobResourceContents:

1
2
3
4
5
6
7
8
from mcp.types import BlobResourceContents
import base64

@mcp.resource("image://{filename}")
async def get_image(filename: str) -> BlobResourceContents:
    with open(f"images/{filename}", "rb") as f:
        data = base64.b64encode(f.read()).decode()
    return BlobResourceContents(blob=data, mimeType="image/png")

LangChain Integration LangChain 集成

The langchain-mcp-adapters library converts MCP tools into LangChain-compatible tools, making them usable in any LangChain agent.

1
pip install langchain-mcp-adapters langchain-openai langgraph
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o")

async def main():
    server_params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Convert MCP tools to LangChain tools
            tools = await load_mcp_tools(session)

            # Create a ReAct agent with MCP tools
            agent = create_react_agent(model, tools)
            result = await agent.ainvoke({
                "messages": [{"role": "user", "content": "What is 15 + 27?"}]
            })
            print(result["messages"][-1].content)

asyncio.run(main())

For multi-server setups:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_mcp_adapters.client import MultiServerMCPClient

async def main():
    async with MultiServerMCPClient({
        "math": {
            "command": "python",
            "args": ["math_server.py"],
            "transport": "stdio",
        },
        "weather": {
            "url": "http://localhost:8000/sse",
            "transport": "sse",
        }
    }) as client:
        tools = client.get_tools()
        agent = create_react_agent(model, tools)
        result = await agent.ainvoke({
            "messages": [{"role": "user", "content": "What's the weather in Beijing?"}]
        })
        print(result["messages"][-1].content)

Remote Deployment 远程部署

SSE Transport SSE 传输模式

To serve an MCP server over HTTP/SSE:

1
2
3
4
5
6
7
8
9
10
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("RemoteServer")

@mcp.tool()
def greet(name: str) -> str:
    return f"Hello, {name}!"

if __name__ == "__main__":
    mcp.run(transport="sse", host="0.0.0.0", port=8000)

The server exposes two endpoints:

  • GET /sse — SSE stream for server-to-client events
  • POST /messages — client-to-server messages

Test with curl:

1
curl -N http://localhost:8000/sse

Docker Deployment

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/
COPY server.py .

EXPOSE 8000

CMD ["python", "server.py"]

docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: "3.8"
services:
  mcp-server:
    build: .
    ports:
      - "8000:8000"
    environment:
      - API_KEY=${API_KEY}
      - LOG_LEVEL=INFO
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
1
2
docker build -t my-mcp-server .
docker-compose up -d

AWS Deployment (Lambda + ECS)

Stateless — Lambda + API Gateway (stdio/HTTPS)

template.yaml (SAM):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
  MCPServerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: lambda_handler.handler
      Runtime: python3.11
      MemorySize: 512
      Timeout: 30
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /mcp
            Method: post

lambda_handler.py:

1
2
3
4
5
6
7
8
9
10
11
12
import json
from server import mcp  # your FastMCP app

def handler(event, context):
    body = json.loads(event.get("body", "{}"))
    # Process MCP request
    response = mcp.handle_request(body)
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(response)
    }

Stateful — ECS Fargate (SSE)

Use ECS Fargate with an Application Load Balancer for persistent SSE connections. Key configuration:

1
2
3
4
5
6
7
8
9
# ECS Task Definition (excerpt)
ContainerDefinitions:
  - Name: mcp-server
    Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/mcp-server:latest"
    PortMappings:
      - ContainerPort: 8000
    Environment:
      - Name: TRANSPORT
        Value: sse

ALB target group: set deregistration_delay to 60s and use sticky sessions (duration-based) to ensure SSE clients stay connected to the same container.

Aliyun Serverless 阿里云函数计算部署

Aliyun Function Compute (FC 3.0) supports HTTP triggers with SSE for stateless MCP servers.

Prerequisites

1
2
3
4
5
# Install Serverless Devs
npm install -g @serverless-devs/s

# Configure credentials
s config add --AccessKeyID <id> --AccessKeySecret <secret> --alias default

s.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
edition: 3.0.0
name: mcp-server

resources:
  mcp-server:
    component: fc3

    props:
      region: cn-hangzhou
      functionName: mcp-server
      description: MCP Server via SSE
      runtime: python3.10
      code: ./
      handler: server.handler
      memorySize: 512
      timeout: 60
      environmentVariables:
        TRANSPORT: sse

      triggers:
        - triggerName: http-trigger
          triggerType: http
          triggerConfig:
            authType: anonymous
            methods:
              - GET
              - POST

Adapt server.py for FC’s HTTP event format:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from mcp.server.fastmcp import FastMCP
import json

mcp = FastMCP("AliyunMCPServer")

@mcp.tool()
def hello(name: str) -> str:
    return f"Hello from Aliyun, {name}!"

def handler(environ, start_response):
    """WSGI-compatible handler for Aliyun FC HTTP trigger."""
    # Route SSE vs POST
    path = environ.get("PATH_INFO", "")
    if environ.get("REQUEST_METHOD") == "GET" and path == "/sse":
        return mcp.handle_sse(environ, start_response)
    elif environ.get("REQUEST_METHOD") == "POST" and path == "/messages":
        return mcp.handle_post(environ, start_response)
    else:
        start_response("404 Not Found", [])
        return [b"Not Found"]

Deploy and test:

1
2
3
4
5
6
7
8
# Deploy
s deploy

# Get endpoint URL
s info

# Test with Inspector
npx @modelcontextprotocol/inspector --sse https://<fc-endpoint>/sse

Security Best Practices 安全最佳实践

RiskMitigation
Credential exposureUse environment variables; never hardcode API keys in server code
Input injectionValidate all tool inputs with Pydantic before executing
Path traversalValidate file paths; use pathlib.Path.resolve() and check against allowed roots
Privilege escalationFollow least-privilege principle; tools should only access what they need
Logging sensitive dataNever log API keys, passwords, or PII; sanitize error messages
Unauthenticated SSEAdd auth middleware (API key header, OAuth token) before exposing SSE publicly

Example — path validation in a file-access tool:

1
2
3
4
5
6
7
8
9
10
from pathlib import Path

ALLOWED_ROOT = Path("/data/safe")

@mcp.tool()
def read_file(filename: str) -> str:
    target = (ALLOWED_ROOT / filename).resolve()
    if not str(target).startswith(str(ALLOWED_ROOT)):
        raise ValueError("Path traversal detected")
    return target.read_text()

Example — API key validation middleware:

1
2
3
4
5
6
7
8
9
10
11
from mcp.server.fastmcp import FastMCP
import os

mcp = FastMCP("SecureServer")

@mcp.middleware()
async def auth_check(request, call_next):
    api_key = request.headers.get("X-API-Key")
    if api_key != os.environ["SERVER_API_KEY"]:
        raise PermissionError("Invalid API key")
    return await call_next(request)

Key Takeaways

  • FastMCP (@mcp.tool(), @mcp.resource(), @mcp.prompt()) is the fastest way to build an MCP server — minimal boilerplate, full type-hint-driven schemas.
  • Use stdio transport for local/desktop integrations; use SSE for any remote or multi-user scenario.
  • MCP Inspector (npx @modelcontextprotocol/inspector) is the primary debug tool — test resources and tools interactively before wiring to a real LLM.
  • The client-side agentic loop: list tools → convert to LLM format → let LLM decide → execute tool → feed results back → final response.
  • Sampling callbacks invert the flow: the server can request LLM generation, enabling pre/post hooks and multi-step server-side reasoning.
  • For production: use Docker + ECS (SSE) for stateful connections, or Lambda + API Gateway for stateless request-per-call workloads.
  • Always validate inputs at the tool boundary, restrict file paths, and keep credentials in environment variables.

References

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.