Model Context Protocol | How to Build & Deploy Remote MCP Servers
An enterprise guide to building, hosting, refactoring, and deploying remote Model Context Protocol (MCP) servers. This note covers transport layer differences, standard deployments using mcp.cloud, asynchronous database handling with aio-sqlite, and workarounds for host-level platform constraints using local proxy architecture.
1. Local vs. Remote MCP Servers: Core Architectural Mental Model
The Model Context Protocol (MCP) decouples Large Language Model (LLM) hosts from tools, data resources, and prompts. Depending on where the MCP server runs, servers are categorized into Local MCP Servers and Remote MCP Servers.
Real-Life Analogy
- Local MCP Server: Like a local offline calculator app on your desktop. It runs using your computer's local CPU, responds instantly without internet access, but cannot be accessed by anyone else or handle heavy remote data tasks.
- Remote MCP Server: Like a web API hosted on cloud infrastructure. Anyone on the internet with permission can connect to it. It offloads processing to cloud hardware and serves thousands of concurrent users, at the cost of network latency.
Key Trade-Offs
2. Building a Minimal Remote MCP Server: Code & Configuration
Writing a remote MCP server using FastMCP (st-mcp / mcp SDK) requires minimal code alterations compared to a local server. The fundamental difference lies in changing the transport protocol from stdio to http (Streamable HTTP) and specifying network bindings (host and port).
Step-by-Step Setup
- Install Package Manager: Ensure
uvis installed globally.BASHpip install uv - Initialize Workspace: Create a project folder and initialize standard MCP environment dependencies.
BASH
mkdir test-remote-mcp-server cd test-remote-mcp-server uv init . uv add mcp
Code Implementation (main.py)
import random
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP server instance
mcp = FastMCP("Simple Calculator Server")
@mcp.tool()
def add_numbers(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
@mcp.tool()
def generate_random_number(min_value: int, max_value: int) -> int:
"""Generate a random integer within a specified range [min_value, max_value]."""
return random.randint(min_value, max_value)
@mcp.resource("resource://server-info")
def get_server_info() -> str:
"""Provide static context information about this remote server."""
return "Simple Calculator Remote MCP Server - Version 1.0"
if __name__ == "__main__":
# CRITICAL CHANGE FOR REMOTE SERVERS:
# Set transport to 'http', host to '0.0.0.0' (accepts external connections), and assign a port.
mcp.run(
transport="http",
host="0.0.0.0",
port=8000
)
Note: While local servers execute via
mcp.run()(which defaults tostdio), remote servers explicitly specifytransport="http"along with host binding0.0.0.0to listen across public or containerized network interfaces.
3. Testing and Debugging Remote Servers via MCP Inspector
Before cloud deployment, verify local server health, tool schemas, and resource discovery using the built-in MCP Inspector UI.
-
Start the Remote Server Locally:
BASHuv run main.pyTerminal Output confirmation:
CODEINFO: Started server process on http://0.0.0.0:8000 INFO: Transport stream set to Streamable HTTP -
Launch MCP Inspector in a Parallel Terminal:
BASHuv run mcp dev main.py -
Inspector Connection Workflow:
Structure & Concept Flow
4. Deploying Remote MCP Servers to mcp.cloud (FastMCP Cloud)
mcp.cloud provides managed hosting specifically optimized for Streamable HTTP MCP servers.
Step-by-Step Deployment Pipeline
Local Environment
|
| 1. Git Commit & Push
v
GitHub Repository (`test-remote-mcp-server`)
|
| 2. Webhook trigger on new push
v
mcp.cloud Hosting Platform
|
| 3. Automatically builds dependencies via uv
| 4. Exposes public HTTPS endpoint
v
Public Endpoint: https://<random-id>.mcp.cloud/mcp
-
Push Repository to GitHub:
BASHgit init git add . git commit -m "Initial commit - Remote MCP Server" git branch -M main git remote add origin https://github.com/your-username/test-remote-mcp-server.git git push -u origin main -
Configure Host on
mcp.cloud:- Sign in to mcp.cloud.
- Click Deploy from your own code.
- Link your GitHub account and select
test-remote-mcp-server. - Configure deployment settings:
- Entry Point:
main.py - Authentication: None (Public endpoints for basic testing).
- Discoverable: Optional (List publicly on FastMCP directory).
- Entry Point:
- Click Deploy.
5. Integrating Remote Servers with Claude Desktop (Pro Tier)
For users with Claude Desktop Pro subscriptions, native remote HTTP MCP connectors can be added directly via the application UI.
- Open Claude Desktop Settings Connectors.
- Click Add Custom Connector.
- Fill in configuration details:
- Name:
Nitish Test Remote Server - URL:
https://<your-subdomain>.mcp.cloud/mcp
- Name:
- Confirm addition. Claude Desktop will inspect tools remotely via SSE/HTTP.
6. Upgrading to the Expense Tracker: Real-World Pitfalls & Production Fixes
Transitioning complex applications (e.g., an Expense Tracker with persistent local SQLite storage) from local execution to cloud serverless/container hosts introduces critical deployment hurdles.
Codebase Migration (main.py & categories.json)
Porting the expense tracker requires adding expenses.db management and schema setup:
import sqlite3
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Expense Tracker Remote")
def init_db():
conn = sqlite3.connect("expenses.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
amount REAL,
category TEXT,
description TEXT
)
""")
conn.commit()
conn.close()
@mcp.tool()
def add_expense(date: str, amount: float, category: str, description: str) -> str:
"""Record a new expense into the database."""
conn = sqlite3.connect("expenses.db")
cursor = conn.cursor()
cursor.execute(
"INSERT INTO expenses (date, amount, category, description) VALUES (?, ?, ?, ?)",
(date, amount, category, description)
)
conn.commit()
conn.close()
return "Expense recorded successfully."
Production Pitfall 1: SQLite Read-Only Filesystem Errors
Symptom: When running on remote cloud containers (mcp.cloud or Render), executing add_expense throws:
sqlite3.OperationalError: attempt to write a readonly database
Root Cause: Containerized runtime environments restrict root directory file creation or lock relative file paths (./expenses.db) in read-only layers.
Fix: Explicitly define persistent dynamic storage paths with execution permissions, ensuring directory paths exist prior to opening connection pools.
import os
import sqlite3
# Define robust directory path in writeable application directory
DB_DIR = os.path.join(os.path.expanduser("~"), ".expense_mcp")
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, "expenses.db")
def get_connection():
return sqlite3.connect(DB_PATH)
7. Asynchronous Architecture: Solving Blocking I/O with aio-sqlite
Production Pitfall 2: Synchronous Blocking Bottlenecks
Standard synchronous Python code handles requests sequentially ( wait times for parallel clients).
When 10 users hit a synchronous remote MCP server simultaneously, database operations block execution threads. Client 2 stalls until Client 1 releases database handles.
Solution: Refactoring to Non-Blocking Async (aio-sqlite)
By leveraging Python's asyncio event loop and aio-sqlite, I/O operations yield control back to the loop, permitting concurrent request processing.
import os
import aio-sqlite
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Async Expense Tracker Remote")
DB_DIR = os.path.join(os.path.expanduser("~"), ".expense_mcp")
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, "expenses.db")
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
amount REAL,
category TEXT,
description TEXT
)
""")
await db.commit()
@mcp.tool()
async def add_expense(date: str, amount: float, category: str, description: str) -> str:
"""Asynchronous tool for writing expenses concurrently."""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO expenses (date, amount, category, description) VALUES (?, ?, ?, ?)",
(date, amount, category, description)
)
await db.commit()
return f"Successfully added expense: {description} (${amount})"
@mcp.tool()
async def list_expenses(start_date: str, end_date: str) -> str:
"""Asynchronous query for retrieving expenses."""
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT date, amount, category, description FROM expenses WHERE date BETWEEN ? AND ?",
(start_date, end_date)
) as cursor:
rows = await cursor.fetchall()
return str(rows)
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)
8. Bypassing Free Tier Restrictions: The Local Proxy Server Pattern
The Problem
Claude Desktop Free Tier does not natively expose the Add Custom Connector HTTP configuration UI. It only supports stdio local executable definitions via claude_desktop_config.json.
Architecture of the Proxy Workaround
Create a Local MCP Proxy Server on your client machine. Claude Desktop talks to the Proxy locally over standard stdio. The Proxy translates and routes all requests over Streamable HTTP to the cloud remote server (mcp.cloud).
Proxy Server Implementation (proxy_main.py)
from mcp.server.fastmcp import FastMCP
# Define local wrapper proxy server instance
mcp = FastMCP("Local Proxy Server")
# Remote Target Endpoint Hosted on mcp.cloud
REMOTE_MCP_URL = "https://your-assigned-id.mcp.cloud/mcp"
# Wrap remote server connection within local stdio interface
proxy_app = FastMCP.as_proxy(
remote_url=REMOTE_MCP_URL,
name="Nitish Remote Proxy"
)
if __name__ == "__main__":
# Local proxy runs via default stdio transport!
proxy_app.run()
Installation Command into Claude Desktop
Install the proxy directly using FastMCP automated CLI configuration:
uv run fastmcp install claude-desktop proxy_main.py
Debugging Configuration Pathing (
which uv): Auto-generatedclaude_desktop_config.jsonfiles may reference non-absolute executable paths (uv). If Claude Desktop reports “Proxy Server Failed to Start”, replace command references with full system binary paths:JSON{ "mcpServers": { "Nitish Remote Proxy": { "command": "/Users/username/.local/bin/uv", "args": ["run", "--with", "mcp", "proxy_main.py"] } } }
9. Critical Enterprise Edge Cases: Multi-Tenancy & Authentication Gaps
While our remote expense tracker is fully deployed and accessible globally, it exhibits a major structural flaw when used in production multi-user scenarios.
Flaw Breakdown
- Data Leakage Across Tenants:
- When
User Aqueries "Show my September expenses", the server runsSELECT * FROM expenses. - Because the database lacks a
user_idcolumn,User Areceives expense data belonging toUser B.
- When
- Missing Identity Verification (Authentication):
- LLMs interface directly with SQL layers without identity headers.
- Trusting user-declared strings (e.g., "I am user 123") without OAuth/JWT validation creates high vulnerability to authorization bypass.
Necessary Next-Step Paradigms
To scale to production enterprise standards, MCP architectures must introduce:
- Session Management: Binding distinct runtime contexts per host connection.
- OAuth2 / JWT Authentication Tokens: Passing encrypted identity metadata within HTTP headers during protocol handshake.
- Elicitation & Client Sampling: Allowing remote MCP servers to securely request user confirmation or run client-side model reasoning loops before committing database transactions.
Summary and Key Takeaways
- Transport Layer Defines Local vs. Remote: Changing an MCP server from local to remote requires switching from
stdiotransport tohttp(Streamable HTTP/SSE) and binding to0.0.0.0. - Zero Paradigm Shift in Tool Code: The internal logic of tools, resources, and prompts remains identical whether declared for local or remote runtime environments.
- FastMCP Cloud Deployment Workflow: Remote servers can be deployed for free via
mcp.cloudby pushing python projects to GitHub with an entry-point configuration (main.py). - Persistent Path Fixes for Container Hosting: Ephemeral containerized builds lock file systems. Database paths must be explicitly routed to dynamic writeable directories (
os.path.expanduser). - Non-Blocking Concurrency via Async: Synchronous SQLite connections block processing threads under multi-user workloads. Upgrading to
aio-sqlitewithasync/awaitenables non-blocking parallel operations. - The Local Proxy Pattern: Users without Claude Desktop Pro can connect to HTTP remote servers by running a lightweight local proxy (
FastMCP.as_proxy) viastdio. - The Multi-Tenancy Authentication Imperative: Unauthenticated remote MCP servers share storage contexts across all clients. Enterprise deployment requires explicit multi-tenant schema isolation (
user_id) backed by robust authentication layers.