Anthropic released Model Context Protocol in late 2024 to solve a real problem: every AI agent needs custom code to connect to data sources. MCP standardizes this. Here's what it is and how to use it with TypeScript.
The Problem MCP Solves
Before MCP, connecting an AI agent to your database looked like this:
// Custom integration for Agent A
async function agentAGetData() {
const db = new DatabaseClient();
return await db.query('SELECT * FROM users');
}
// Different custom integration for Agent B
async function agentBGetData() {
const conn = connectToDatabase();
return conn.fetchUsers();
}
Every agent, every tool, every integration required custom code. MCP creates a standard protocol so you write the integration once and any MCP-compatible agent can use it.
Think of it like OAuth did for authentication—instead of building login flows for every app, you implement OAuth once and everyone can use it.
What is MCP?
Model Context Protocol is:
- A standard way for AI agents to request data from sources
- A client-server architecture where servers expose data, clients (agents) consume it
- Transport agnostic - works over stdio, HTTP, WebSocket
In practice: You write an MCP server that exposes your filesystem, database, or API. Any agent that speaks MCP can then access that data without custom integration code.
When to Use MCP
Use MCP when:
- Multiple agents need the same data source
- You want reusable integrations
- You're building tools others will use
Skip MCP when:
- You have one agent with 2-3 simple tools
- Custom integration is faster for your use case
- You're prototyping and speed matters
MCP adds structure. That's great for maintainability, but it's overhead for simple projects.
Quick Start: Your First MCP Server
We'll build a filesystem server that lets agents read files.
Setup
mkdir mcp-filesystem-server
cd mcp-filesystem-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
npx tsc --init
Create the Server
Create src/index.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
// Create the MCP server
const server = new Server(
{
name: 'filesystem-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Define what tools this server provides
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'read_file',
description: 'Read the contents of a file',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file to read',
},
},
required: ['path'],
},
},
{
name: 'list_directory',
description: 'List files in a directory',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the directory',
},
},
required: ['path'],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'read_file') {
const filePath = args.path as string;
const content = await fs.readFile(filePath, 'utf-8');
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
if (name === 'list_directory') {
const dirPath = args.path as string;
const files = await fs.readdir(dirPath);
const fileList = files.join('\n');
return {
content: [
{
type: 'text',
text: `Files in ${dirPath}:\n${fileList}`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Filesystem MCP server running on stdio');
}
main().catch(console.error);
Build and Test
# Build
npx tsc
# Test (the server communicates over stdio)
node dist/index.js
The server is now running and waiting for MCP requests over stdin/stdout.
What Just Happened?
You created an MCP server with two capabilities:
- ListTools: Tells clients what tools are available
- CallTool: Executes tools when requested
When a client (like Claude Desktop) connects:
- It asks: "What tools do you have?"
- Server responds: "I have read_file and list_directory"
- Client requests: "Run read_file with path=/example.txt"
- Server executes and returns the file contents
Connecting to Claude Desktop
Claude Desktop has built-in MCP support. To use your server:
1. Create MCP Configuration
On macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
On Windows: %APPDATA%/Claude/claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "node",
"args": ["/absolute/path/to/your/dist/index.js"]
}
}
}
2. Restart Claude Desktop
After restarting, Claude can now read files and list directories using your MCP server.
Ask Claude: "What files are in my Documents folder?" and it will use your MCP server to answer.
Pre-Built MCP Servers
You don't have to write every integration. Several official servers exist:
# Install pre-built servers
npm install -g @modelcontextprotocol/server-filesystem
npm install -g @modelcontextprotocol/server-github
npm install -g @modelcontextprotocol/server-postgres
Configure them in claude_desktop_config.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/Documents"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "your-token-here"
}
}
}
}
Now Claude can access your filesystem and GitHub without you writing any integration code.
Building a Database MCP Server
Let's build something more practical—a PostgreSQL server:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { Client } from 'pg';
const dbClient = new Client({
connectionString: process.env.DATABASE_URL,
});
await dbClient.connect();
const server = new Server(
{
name: 'postgres-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'query',
description: 'Execute a SQL query (SELECT only for safety)',
inputSchema: {
type: 'object',
properties: {
sql: {
type: 'string',
description: 'SQL query to execute',
},
},
required: ['sql'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'query') {
const sql = args.sql as string;
// Safety check - only allow SELECT
if (!sql.trim().toLowerCase().startsWith('select')) {
return {
content: [
{
type: 'text',
text: 'Only SELECT queries are allowed',
},
],
isError: true,
};
}
try {
const result = await dbClient.query(sql);
return {
content: [
{
type: 'text',
text: JSON.stringify(result.rows, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Query error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
Now any MCP client can query your database safely.
MCP Client (Connecting from Your Agent)
Want to use MCP servers from your own agent? Use the client SDK:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// Connect to an MCP server
const transport = new StdioClientTransport({
command: 'node',
args: ['./path/to/mcp-server.js'],
});
const client = new Client(
{
name: 'my-agent',
version: '1.0.0',
},
{
capabilities: {},
}
);
await client.connect(transport);
// List available tools
const tools = await client.listTools();
console.log('Available tools:', tools);
// Call a tool
const result = await client.callTool({
name: 'read_file',
arguments: {
path: '/path/to/file.txt',
},
});
console.log('Result:', result);
This lets you build agents that use MCP servers as tools.
Common Patterns
Adding Resources (Not Just Tools)
MCP servers can expose resources (data) and tools (actions):
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'config://app/settings',
name: 'App Settings',
mimeType: 'application/json',
},
],
};
});
// Read a specific resource
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'config://app/settings') {
const settings = { theme: 'dark', notifications: true };
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(settings, null, 2),
},
],
};
}
return { contents: [] };
});
Resources are for data that agents read; tools are for actions they execute.
Security Considerations
MCP servers have full system access. Consider:
- Validate inputs: Don't trust agent requests blindly
- Limit file access: Only expose specific directories
- Read-only by default: Separate read and write tools
- No credential exposure: Handle auth server-side
- Rate limiting: Prevent abuse
Example safe file access:
const ALLOWED_DIR = '/safe/directory';
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'read_file') {
const requestedPath = request.params.arguments.path as string;
const resolvedPath = path.resolve(ALLOWED_DIR, requestedPath);
// Prevent directory traversal
if (!resolvedPath.startsWith(ALLOWED_DIR)) {
return {
content: [{ type: 'text', text: 'Access denied' }],
isError: true,
};
}
// Safe to proceed
const content = await fs.readFile(resolvedPath, 'utf-8');
return { content: [{ type: 'text', text: content }] };
}
});
When MCP Makes Sense
Good fit:
- You're building tools for multiple agents
- You want a reusable GitHub/database/filesystem integration
- You're part of a team with many agent projects
Overkill:
- Single prototype with a few custom tools
- One-off integration that won't be reused
- Performance-critical paths (MCP adds overhead)
MCP trades flexibility for structure. That's great when you need consistency across projects, but it's extra work for simple cases.
What's Next?
The MCP ecosystem is early but growing. More servers are being published, and frameworks are adding MCP support.
To learn more:
For your projects:
- Start with pre-built servers when available
- Write custom servers for your specific data sources
- Share useful servers—the ecosystem benefits from contributions
Wrapping Up
MCP standardizes how AI agents access data. Instead of building custom integrations for every agent, you write an MCP server once and any compatible agent can use it.
It's useful when you have multiple agents or want reusable integrations. For simple projects, custom tools are often faster.
The TypeScript SDK makes building MCP servers straightforward. The pattern is consistent: define tools, handle requests, return results. Everything else is application-specific logic.
If you're building agent infrastructure, MCP is worth learning. If you're building a single agent, evaluate whether the standardization benefits outweigh the added complexity.
Top comments (0)