Build Your First MCP Server — Connect AI to Real Data
AI assistants are powerful, but they're limited to what they already know. The Model Context Protocol (MCP) changes that. It's an open standard that lets you safely connect AI models to your own data, tools, and services.
In this tutorial, you'll build a working MCP server from scratch. By the end, you'll understand the protocol's core concepts and have a server that exposes real data to AI assistants.
What Is MCP?
MCP (Model Context Protocol) is an open protocol created by Anthropic that standardizes how AI models interact with external systems. Think of it as a universal adapter between AI and your infrastructure.
Instead of building custom integrations for each AI platform, MCP gives you one interface that works across compatible clients. You build once, connect everywhere.
Why Build an MCP Server?
- Secure data access: Control exactly what the AI can see and do
- Reusable integrations: One server works with any MCP-compatible client
- Standardized patterns: Resources, tools, and prompts follow consistent conventions
- Local or remote: Run servers on your machine or deploy them as services
MCP Architecture Overview
MCP uses a client-server model:
┌─────────────┐ ┌─────────────┐
│ MCP Client │ ◄─────► │ MCP Server │
│ (AI App) │ │ (Your Code)│
└─────────────┘ └─────────────┘
The client is the AI application (like an assistant or IDE plugin). The server is your code that exposes capabilities.
MCP servers expose three types of capabilities:
- Resources: Read-only data the AI can access (files, database records, API responses)
- Tools: Actions the AI can execute (run a query, send a message, trigger a workflow)
- Prompts: Pre-built templates that help users interact with your server
Prerequisites
Before you start, make sure you have:
- Node.js 18+ or Python 3.10+
- Basic understanding of async programming
- A code editor (VS Code works well)
- An MCP-compatible client to test with (like Claude Desktop or an MCP inspector)
Step 1: Set Up Your Project
We'll build a simple server that exposes system information and can run basic commands. This demonstrates the core patterns you'll use for any integration.
Create a new project directory:
mkdir mcp-server-demo
cd mcp-server-demo
npm init -y
npm install @modelcontextprotocol/sdk
Create your main server file:
touch index.js
Step 2: Initialize the MCP Server
Here's the basic server structure:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'demo-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Handle resource listing
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'system://info',
name: 'System Information',
description: 'Current system status and metrics',
mimeType: 'application/json',
},
],
};
});
// Handle resource reading
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'system://info') {
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
platform: process.platform,
nodeVersion: process.version,
uptime: process.uptime(),
memory: process.memoryUsage(),
}, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_environment',
description: 'Get environment variables (safe subset)',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'The environment variable name',
},
},
required: ['key'],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'get_environment') {
const key = args.key;
// Only allow safe variables
const allowed = ['NODE_ENV', 'PATH', 'USER', 'HOME'];
if (!allowed.includes(key)) {
return {
content: [{ type: 'text', text: `Variable ${key} is not accessible` }],
isError: true,
};
}
const value = process.env[key] || '(not set)';
return {
content: [{ type: 'text', text: `${key}=${value}` }],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
Step 3: Understanding the Core Patterns
Resources: Read-Only Data
Resources are identified by URIs and represent data the AI can read. Think of them as endpoints the AI can query:
file://path/to/file— Local filespostgres://database/table— Database tablesapi://service/endpoint— API responses
Your server defines what URIs it supports and what data they return.
Tools: Executable Actions
Tools let the AI perform actions. Each tool has:
- A name and description (the AI uses these to decide when to call it)
- An input schema (JSON Schema defining valid arguments)
- Execution logic (your code that runs when called)
Tools can modify data, trigger workflows, or interact with external systems. Always validate inputs and implement appropriate access controls.
Prompts: Guided Interactions
Prompts are templates that help users get started. They're less commonly used initially but valuable for complex servers:
import { ListPromptsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: 'analyze_system',
description: 'Analyze current system health',
arguments: [
{
name: 'detail_level',
description: 'How detailed the analysis should be',
required: false,
},
],
},
],
};
});
Step 4: Configure Your MCP Client
To test your server, you need to configure an MCP client. For Claude Desktop, add this to your config file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"demo-server": {
"command": "node",
"args": ["/path/to/your/mcp-server-demo/index.js"]
}
}
}
Restart Claude Desktop and your server will be available.
Step 5: Test Your Server
You can also use the MCP Inspector for debugging:
npx @modelcontextprotocol/inspector
This opens a web interface where you can:
- List available resources and tools
- Test tool execution
- View server logs
- Debug connection issues
Try asking the AI:
- "What system information is available?"
- "Can you check my NODE_ENV variable?"
Building Real Integrations
The demo above is a starting point. Here's how to build production servers:
Database Integration
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'postgres://users',
name: 'Users Table',
description: 'Application user records',
mimeType: 'application/json',
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'postgres://users') {
const result = await pool.query('SELECT id, name, email FROM users LIMIT 100');
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(result.rows, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
API Integration
import fetch from 'node-fetch';
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_issue',
description: 'Create a GitHub issue',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string' },
body: { type: 'string' },
labels: { type: 'array', items: { type: 'string' } },
},
required: ['title'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'create_issue') {
const response = await fetch('https://api.github.com/repos/owner/repo/issues', {
method: 'POST',
headers: {
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
},
body: JSON.stringify(args),
});
const data = await response.json();
return {
content: [{ type: 'text', text: `Issue created: ${data.html_url}` }],
};
}
throw new Error(`Unknown tool: ${name}`);
});
Security Best Practices
When building MCP servers, security is critical:
- Validate all inputs: Never trust AI-generated arguments without validation
- Limit tool permissions: Only expose what's necessary
- Use environment variables: Store secrets securely, never in code
- Implement rate limiting: Prevent abuse of your tools
- Log all operations: Audit what the AI does through your server
- Run with minimal privileges: Don't run servers as root
Deployment Options
Local Development
Run servers locally during development. This is fastest for iteration.
Docker Deployment
Containerize your server for consistent deployment:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]
Remote Servers
MCP supports remote connections via HTTP. For production, you might run servers as separate services that clients connect to over the network.
Common Pitfalls to Avoid
- Over-exposing data: Start minimal, add capabilities as needed
- Ignoring errors: Always handle failures gracefully and return clear error messages
- Blocking operations: Use async patterns to avoid freezing the server
- Missing documentation: Document what each resource and tool does
- No testing: Test your server with the MCP Inspector before connecting to production clients
Next Steps
You now have a working MCP server. Here's where to go from here:
- Explore the SDK: Check the official @modelcontextprotocol/sdk for TypeScript and Python libraries
- Study existing servers: Look at open-source MCP servers for patterns and best practices
- Build something useful: Connect your database, API, or internal tools
- Share your work: The MCP ecosystem grows when developers share servers
Conclusion
MCP gives you a standardized way to extend AI capabilities with your own data and tools. By building an MCP server, you're not just connecting one AI — you're creating a reusable integration that works across the ecosystem.
Start small with a simple server like the one in this tutorial. Then expand to connect your real data sources. The patterns stay the same whether you're exposing system info or enterprise databases.
The future of AI is composable. MCP is how you make your systems part of that future.
Sources:
- Model Context Protocol Documentation — https://modelcontextprotocol.io
- MCP SDK (TypeScript) — https://github.com/modelcontextprotocol/typescript-sdk
- Anthropic MCP Announcement — https://www.anthropic.com/news/model-context-protocol
Follow for more hands-on engineering content: https://buildwithabdallah.com
#AI #dev #BuildWithAbdallah