DEV Community

Cover image for Model Context Protocol (MCP) for TypeScript Developers: A 10-Minute Guide
Raju Dandigam
Raju Dandigam

Posted on

Model Context Protocol (MCP) for TypeScript Developers: A 10-Minute Guide

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();
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Build and Test

# Build
npx tsc


# Test (the server communicates over stdio)
node dist/index.js

Enter fullscreen mode Exit fullscreen mode

The server is now running and waiting for MCP requests over stdin/stdout.

What Just Happened?

You created an MCP server with two capabilities:

  1. ListTools: Tells clients what tools are available
  2. CallTool: Executes tools when requested

When a client (like Claude Desktop) connects:

  1. It asks: "What tools do you have?"
  2. Server responds: "I have read_file and list_directory"
  3. Client requests: "Run read_file with path=/example.txt"
  4. 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"]
   }
 }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"
     }
   }
 }
}

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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: [] };
});

Enter fullscreen mode Exit fullscreen mode

Resources are for data that agents read; tools are for actions they execute.

Security Considerations

MCP servers have full system access. Consider:

  1. Validate inputs: Don't trust agent requests blindly
  2. Limit file access: Only expose specific directories
  3. Read-only by default: Separate read and write tools
  4. No credential exposure: Handle auth server-side
  5. 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 }] };
 }
});

Enter fullscreen mode Exit fullscreen mode

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:

  1. Start with pre-built servers when available
  2. Write custom servers for your specific data sources
  3. 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)