Building Your First MCP Server: A Step-by-Step Guide
The AI landscape is evolving rapidly, with models like Claude becoming increasingly sophisticated in their reasoning and capabilities. However, even the most advanced AI models face a fundamental limitation: they're isolated from your data. This creates a significant barrier, as every data source requires its own custom integration, making truly connected systems difficult to scale.
Enter the Model Context Protocol (MCP).
Released by Anthropic in late 2024, MCP is an open standard that provides a universal way to connect AI systems with your data sources. Think of it as the "USB-C port for AI" — a standardized connector between AI models and the systems where your data lives, including databases, content repositories, APIs, and development environments.
Instead of building fragmented, one-off integrations for each combination of AI model and data source, MCP offers a more sustainable approach: a single protocol that enables any MCP-compatible client to connect with any MCP-compatible server.
What You'll Learn in This Guide
In this step-by-step tutorial, we'll build a custom MCP server that connects to a database, allowing Claude (or any other MCP-compatible AI assistant) to:
- Discover the schema of your database
- Execute parameterized queries against your data
- Present formatted results back to the user
By the end of this guide, you'll have a fully functional MCP server running locally and connected to Claude Desktop, enabling powerful AI-powered data analysis while keeping your data secure and under your control.
Who This Guide is For
This guide is designed for developers with:
- Basic programming experience (we'll provide examples in both Python and TypeScript)
- Familiarity with database concepts
- Interest in extending AI capabilities with your own data
No prior experience with MCP is required — we'll cover everything from first principles.
According to Dhanji R. Prasanna, CTO at Block, "Open technologies like the Model Context Protocol are the bridges that connect AI to real-world applications, ensuring innovation is accessible, transparent, and rooted in collaboration." MCP addresses one of the biggest challenges in AI today: connecting models to the data they need to be truly useful.
Understanding MCP Architecture
Before diving into code, let's understand how the Model Context Protocol works. Having this mental model will make the implementation much clearer.
The Core Components
MCP is built around three key components:
- Hosts: Applications like Claude Desktop or IDEs that embed an LLM. Hosts initiate connections with servers.
- Clients: The part of the host application that maintains 1:1 connections with servers. Clients request data from servers and pass it to the LLM.
- Servers: Your custom implementation that connects to external systems like databases, file systems, or APIs. Servers provide context, tools, and prompts to clients.
Here's a simplified visualization of how these components interact:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ │ │ │ │ │
│ Claude │ │ MCP Client │ │ Your Custom │
│ (or other │◄────┤ (built into │◄────┤ MCP Server │◄────► Database
│ AI model) │ │ the host) │ │ │
│ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Server Capabilities
MCP servers can provide three main types of capabilities:
- Resources: File-like data that can be read by clients. Think of these as static or semi-static data like database schemas, file contents, or API responses. Resources help LLMs understand what data is available.
- Tools: Functions that can be called by the LLM (with user approval). These are the dynamic operations your server can perform, like running a database query, calling an API, or processing data. Tools can modify state or interact with external systems.
- Prompts: Pre-written templates that help users accomplish specific tasks. These are reusable workflows that clients can easily surface to users and LLMs, standardizing common interactions.
The Protocol
Under the hood, MCP uses JSON-RPC 2.0 as its wire format. Communication happens through four message types:
- Requests: Messages that expect a response.
{ "jsonrpc": "2.0", "id": 1, "method": "query_database", "params": { "sql": "SELECT * FROM users LIMIT 5" } }
- Notifications: One-way messages that don't expect a response.
{ "jsonrpc": "2.0", "method": "log_event", "params": { "message": "User accessed database" } }
- Results: Successful responses to requests.
{ "jsonrpc": "2.0", "id": 1, "result": { "rows": [ { "id": 1, "name": "John" }, { "id": 2, "name": "Jane" } ] } }
- Errors: Responses to failed requests.
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "Database connection failed" } }
Transport Mechanisms
MCP supports multiple transport mechanisms:
- Stdio transport for local processes
- HTTP with Server-Sent Events (SSE) for remote connections
For our database server, we'll use the local stdio transport, which is the simplest and most secure option for development.
Why This Architecture Matters
This architecture has several advantages:
- Security: Data remains under your control. MCP servers run locally by default, so sensitive data never leaves your system.
- Standardization: Once you build an MCP server, it can be used with any MCP-compatible client, not just Claude.
- Composability: You can create multiple MCP servers for different data sources and use them together.
- Scalability: As your needs grow, the architecture can scale with you, from simple local deployments to enterprise-wide systems.
If you want to explore MCP architecture in more depth, check out the official MCP documentation or our earlier article on What is the Model Context Protocol.
Prerequisites
Before we start building our MCP server, let's make sure you have everything you need:
Required Knowledge
- Basic programming experience in either Python or JavaScript/TypeScript
- Familiarity with database concepts (tables, queries, schemas)
- Comfort using the command line
Software Requirements
- Python 3.10 or higher (for Python implementation)
- Node.js 18+ and npm (for TypeScript implementation)
- Claude Desktop app installed (for testing your server)
For our examples, we'll be using SQLite as our database since it's lightweight and doesn't require a separate server. However, the concepts will apply to any database system (PostgreSQL, MySQL, etc.).
At the time of writing (February 2025), MCP is supported in Claude Desktop and several IDEs like Zed, Replit, and Sourcegraph. The Python SDK (v1.3.0+) and TypeScript SDK are the most mature implementations.
Building a Database Connector Server (Python)
In this section, we'll build a Python-based MCP server that connects to a SQLite database. We'll use the Python MCP SDK, which provides a clean and type-safe interface for building MCP servers.
Setting Up Your Environment
First, let's set up our development environment:
# Create a new directory for our project
mkdir mcp-db-connector
cd mcp-db-connector
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install "mcp[cli]" aiosqlite
Creating a Sample Database
Let's create a simple SQLite database for our example:
# Create a file to initialize our database
touch create_sample_db.py
Open create_sample_db.py
and add the following code:
import sqlite3
# Create a connection to a new SQLite database
conn = sqlite3.connect('sample.db')
cursor = conn.cursor()
# Create tables
cursor.execute('''
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
signup_date TEXT NOT NULL
)
''')
cursor.execute('''
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
product TEXT NOT NULL,
amount REAL NOT NULL,
order_date TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers (id)
)
''')
# Insert sample data
customers = [
(1, 'John Smith', 'john@example.com', '2023-01-15'),
(2, 'Sarah Johnson', 'sarah@example.com', '2023-02-20'),
(3, 'Michael Brown', 'michael@example.com', '2023-03-10'),
(4, 'Emma Davis', 'emma@example.com', '2023-04-05'),
(5, 'James Wilson', 'james@example.com', '2023-05-12')
]
orders = [
(1, 1, 'Laptop', 1299.99, '2023-02-01'),
(2, 1, 'Mouse', 25.99, '2023-02-01'),
(3, 2, 'Headphones', 149.99, '2023-03-15'),
(4, 3, 'Monitor', 299.99, '2023-04-20'),
(5, 4, 'Keyboard', 89.99, '2023-05-10'),
(6, 1, 'External Hard Drive', 129.99, '2023-06-05'),
(7, 5, 'Laptop', 1499.99, '2023-06-15'),
(8, 2, 'Printer', 199.99, '2023-07-01'),
(9, 3, 'Webcam', 79.99, '2023-07-10'),
(10, 5, 'Speaker', 129.99, '2023-08-05')
]
cursor.executemany('INSERT INTO customers VALUES (?,?,?,?)', customers)
cursor.executemany('INSERT INTO orders VALUES (?,?,?,?,?)', orders)
# Commit and close
conn.commit()
conn.close()
print("Sample database created successfully!")
Run this script to create the sample database:
python create_sample_db.py
Building the MCP Server
Now, let's create our MCP server that will connect to this database:
# Create a file for our MCP server
touch db_server.py
We'll implement the server code in sections to make it easier to understand:
import os
import json
import asyncio
from typing import Any, Dict, List, Optional
from datetime import datetime
import aiosqlite
from mcp.server.fastmcp import FastMCP
from mcp.resources import Resource, ResourceContent, ResourceType
# Initialize FastMCP server
mcp = FastMCP("sqlite-db-connector")
# Database path
DB_PATH = "sample.db"
# Helper function to convert rows to dictionaries
def dict_factory(cursor, row):
fields = [column[0] for column in cursor.description]
return {key: value for key, value in zip(fields, row)}
# Helper function for database connection
async def get_db():
db = await aiosqlite.connect(DB_PATH)
db.row_factory = dict_factory
return db
Next, let's implement the resource handler that will expose our database schema as resources:
# Resource handler for database schema
@mcp.resource_root()
async def list_resources() -> Dict[str, Resource]:
resources = {}
try:
db = await get_db()
# Get list of tables
query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"
cursor = await db.execute(query)
tables = await cursor.fetchall()
for table in tables:
table_name = table['name']
# Get schema for each table
cursor = await db.execute(f"PRAGMA table_info({table_name});")
columns = await cursor.fetchall()
schema_content = {
"table": table_name,
"columns": columns
}
# Create a resource for each table schema
resource_path = f"tables/{table_name}/schema"
resources[resource_path] = Resource(
type=ResourceType.TEXT,
content=ResourceContent(
text=json.dumps(schema_content, indent=2)
),
name=f"{table_name} Schema",
description=f"Schema information for the {table_name} table"
)
await db.close()
except Exception as e:
print(f"Error listing resources: {e}")
return resources
Now, let's implement our tool for executing SQL queries:
# Tool for executing SQL queries
@mcp.tool()
async def query_database(sql: str) -> str:
"""
Execute a read-only SQL query against the database.
Args:
sql: The SQL query to execute (SELECT queries only)
Returns:
The query results as a formatted string
"""
# Basic security check - only allow SELECT statements
sql = sql.strip()
if not sql.lower().startswith('select'):
return "Error: Only SELECT queries are allowed for security reasons."
try:
db = await get_db()
cursor = await db.execute(sql)
rows = await cursor.fetchall()
await db.close()
# Format the results
if not rows:
return "Query executed successfully, but returned no results."
# Get column names from the first row
columns = rows[0].keys()
# Format as a table
result = "| " + " | ".join(columns) + " |\n"
result += "| " + " | ".join(["---" for _ in columns]) + " |\n"
for row in rows:
result += "| " + " | ".join([str(row[col]) for col in columns]) + " |\n"
return result
except Exception as e:
return f"Error executing query: {str(e)}"
Let's add one more tool for getting detailed information about a specific table:
# Tool for getting table information
@mcp.tool()
async def get_table_info(table_name: str) -> str:
"""
Get detailed information about a specific table.
Args:
table_name: The name of the table to get information about
Returns:
Detailed information about the table structure and sample data
"""
try:
db = await get_db()
# Verify the table exists
cursor = await db.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?;",
(table_name,)
)
if not await cursor.fetchone():
await db.close()
return f"Error: Table '{table_name}' does not exist."
# Get table schema
cursor = await db.execute(f"PRAGMA table_info({table_name});")
columns = await cursor.fetchall()
# Get sample data (first 5 rows)
cursor = await db.execute(f"SELECT * FROM {table_name} LIMIT 5;")
sample_data = await cursor.fetchall()
# Get row count
cursor = await db.execute(f"SELECT COUNT(*) as count FROM {table_name};")
count_result = await cursor.fetchone()
row_count = count_result['count']
await db.close()
# Format the results
result = f"# Table: {table_name}\n\n"
result += f"Row count: {row_count}\n\n"
result += "## Schema\n\n"
result += "| Column | Type | Not Null | Default | Primary Key |\n"
result += "| ------ | ---- | -------- | ------- | ----------- |\n"
for col in columns:
result += f"| {col['name']} | {col['type']} | {'Yes' if col['notnull'] else 'No'} | {col['dflt_value'] or 'NULL'} | {'Yes' if col['pk'] else 'No'} |\n"
result += "\n## Sample Data\n\n"
if sample_data:
# Column headers
result += "| " + " | ".join([col['name'] for col in columns]) + " |\n"
result += "| " + " | ".join(["---" for _ in columns]) + " |\n"
# Data rows
for row in sample_data:
result += "| " + " | ".join([str(row[col['name']]) for col in columns]) + " |\n"
else:
result += "Table contains no data."
return result
except Exception as e:
return f"Error retrieving table information: {str(e)}"
# Start the server when this script is run directly
if __name__ == "__main__":
asyncio.run(mcp.serve())
Running and Testing Your Server
Now let's run our MCP server:
python db_server.py
Your server is now running! But how do we test it? The MCP ecosystem provides a convenient Inspector tool that we can use:
# In a new terminal (keeping the server running)
mcp inspect --transport stdio --command "python db_server.py"
This will open the MCP Inspector in your default web browser. Here, you can:
- See all the resources exposed by your server
- Test the tools your server provides
- Debug any issues that arise
Try out the query_database
tool with a simple SQL query:
SELECT * FROM customers LIMIT 3
If you encounter issues, check the terminal where your server is running for error messages. Remember that your server is running locally, so you can add print statements for debugging if needed.
Connecting to Claude Desktop
To use your server with Claude:
- Download and install Claude Desktop if you haven't already
- Open Claude Desktop and navigate to Settings
- Go to the "Developer" section
- Click on "Edit MCP Configuration"
- Add your server to the configuration:
{
"servers": [
{
"name": "SQLite Database Connector",
"transport": {
"type": "stdio",
"command": "python /path/to/your/db_server.py"
}
}
]
}
Make sure to replace /path/to/your/db_server.py
with the actual path to your script. Save the configuration and restart Claude Desktop.
You can verify the connection is working by opening the Developer Tools in Claude Desktop (Settings → Developer → Open Developer Tools) and checking the console for any connection errors.
Example Interactions with Claude
Once connected, you can ask Claude questions like:
- "What tables are in the database?"
- "Show me the schema of the customers table"
- "How many orders does each customer have?"
- "What's the total amount spent by each customer?"
For the last query, Claude might run a SQL query like:
SELECT c.name, COUNT(o.id) as order_count, SUM(o.amount) as total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY total_spent DESC
Claude will use your MCP server to execute the query and provide a meaningful response that includes the results.
Security Considerations
Our simple example includes basic security measures (only allowing SELECT statements), but in a production environment, you should:
- Implement proper authentication and authorization
- Use parameterized queries to prevent SQL injection
- Limit access to sensitive data
- Consider implementing a read-only database user
- Log all queries for audit purposes
For databases containing sensitive information, consider implementing field-level security or data masking.
MCP servers run locally by default, which provides a good security baseline. However, remote MCP servers require additional security measures. Always follow the MCP security best practices when deploying to production.
Building a Database Server (TypeScript)
If you prefer working with TypeScript, we also have a version for you. The concepts are the same, but the implementation details differ slightly.
To keep this tutorial focused, we'll provide an overview of the TypeScript implementation and link to the complete example on GitHub.
Setting Up Your TypeScript Project
# Create a new directory
mkdir mcp-db-ts
cd mcp-db-ts
# Initialize npm project
npm init -y
# Install dependencies
npm install @mcp/sdk sqlite3 sqlite better-sqlite3 typescript ts-node
Create a tsconfig.json
file:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"outDir": "./dist",
"strict": true
}
}
You can find the complete TypeScript implementation in our MCP TypeScript Examples article.
Advanced Features
Once you have the basic server working, you can extend it with more advanced features:
1. Implementing Prompts
Prompts are pre-written templates that help users accomplish specific tasks. You can add common data analysis prompts to help users explore your database:
@mcp.prompt()
def analyze_sales_data() -> str:
"""
Analyze the sales data in the database.
Guides the user through analyzing sales patterns,
customer behavior, and product performance.
"""
return """
I'll help you analyze the sales data in the database.
First, let's get an overview of the database schema.
What specific aspect of the sales data would you like to explore?
1. Sales trends over time
2. Customer purchase patterns
3. Product performance analysis
4. Custom analysis (specify your requirements)
"""
2. Supporting Multiple Databases
You can extend your server to connect to multiple databases:
@mcp.tool()
async def connect_database(path: str) -> str:
"""
Connect to a different SQLite database file.
Args:
path: Path to the SQLite database file
Returns:
Success or error message
"""
global DB_PATH
if not os.path.exists(path):
return f"Error: Database file {path} does not exist."
try:
DB_PATH = path
# Test connection
db = await get_db()
await db.close()
return f"Successfully connected to database: {path}"
except Exception as e:
return f"Error connecting to database: {str(e)}"
3. Adding Data Visualization Capabilities
You could extend your server to generate visualizations from query results:
@mcp.tool()
async def visualize_data(sql: str, chart_type: str) -> str:
"""
Generate a visualization of query results.
Args:
sql: SQL query to execute (SELECT only)
chart_type: Type of chart (bar, line, pie)
Returns:
URL to generated visualization
"""
# Implementation details omitted for brevity
# You would execute the query, generate a chart, and return a link or base64 image
pass
For more advanced features, check our companion articles:
Deployment Considerations
When moving beyond development to deploying your MCP server in a production environment, consider the following:
Local Deployment
For personal or team use, local deployment is simplest:
- Package your server as a standalone application
- Distribute it to your team with clear installation instructions
- Provide a configuration template for Claude Desktop
Enterprise Deployment
For enterprise scenarios, consider:
- Creating a centralized service that hosts multiple MCP servers
- Implementing proper authentication and authorization
- Setting up monitoring and logging for all MCP interactions
- Establishing governance policies for which data sources can be accessed
Remote MCP Servers
While local deployment is currently the most common approach, remote MCP servers (using HTTP transport) are on the roadmap. This will enable:
- Centralized management of MCP servers
- Integration with cloud-based AI services
- Mobile and web-based clients
As the MCP ecosystem evolves, we expect to see more deployment options, including managed MCP server hosting, cloud-based MCP server registries, and integration with existing enterprise data platforms. Stay up to date on these developments by following our blog.
Future Directions
The Model Context Protocol is still evolving, with several exciting developments on the horizon:
Expanding Ecosystem
- More MCP clients: Beyond Claude Desktop, we're seeing adoption from IDE vendors, productivity tools, and other AI assistants.
- Pre-built servers: The community is building a library of ready-to-use MCP servers for popular data sources.
- Integration frameworks: Tools that make it even easier to create and deploy MCP servers.
Enhanced Capabilities
- Bidirectional streaming: For real-time interactions between LLMs and data sources.
- Multi-modal support: Beyond text to include images, audio, and other data types.
- Enhanced security models: For enterprise-grade data access control.
Get Involved
The MCP community is growing rapidly. Here's how you can contribute:
- Share your MCP servers on GitHub
- Report issues and suggest improvements to the MCP specification
- Join discussions on the MCP Discord
Conclusion
In this guide, we've built a custom MCP server that connects an AI assistant to a database, enabling powerful data exploration and analysis capabilities. This represents just one example of what's possible with the Model Context Protocol.
By providing a standardized way for AI systems to access external data, MCP is breaking down the barriers between powerful AI models and the systems where our data lives. This opens up countless possibilities for building more useful, context-aware AI applications.
As you continue your MCP journey, remember these key points:
- MCP is designed with security in mind, keeping your data under your control
- The architecture is extensible, allowing you to build servers for any data source
- The ecosystem is growing, with more clients and servers being added regularly
We hope this guide has been helpful in getting you started with MCP. If you build something interesting, we'd love to hear about it!
Resources
Official Documentation
Related Articles
- What is the Model Context Protocol
- MCP Applications and Use Cases
- AI Agents in 2025
- Emerging AI Architectures in 2025