Mastering MCP: Building Secure AI Agents with TypeScript
For years, the primary bottleneck in building production-ready AI agents hasn't been the intelligence of the models themselves, but the friction of connecting those models to real-world data. We’ve all been there: writing bespoke 'glue' code, managing complex OAuth flows for simple internal lookups, and worrying about the security implications of exposing sensitive database schemas to a cloud-hosted LLM.
Anthropic’s Model Context Protocol (MCP) changes this dynamic. It provides a standardized, open-source specification for how LLMs interact with external data and tools. By decoupling the model (the 'brain') from the data source (the 'memory' and 'hands'), MCP allows us to build modular, secure, and highly capable agentic systems. In this guide, we’ll explore how to implement MCP using TypeScript to connect an LLM to local resources and internal APIs.
The Problem: Integration Sprawl
Before MCP, every integration was a snowflake. If you wanted Claude to query your Jira tickets and then check a local SQLite database, you had to write custom function-calling logic for both. If you then switched to a different model or framework, you often had to rewrite those integrations.
This 'integration sprawl' leads to three main issues:
- Maintenance Overhead: Every API change requires updating multiple custom shims.
- Security Risks: Hardcoding credentials or over-provisioning access to LLM 'plugins' creates a massive attack surface.
- Lack of Portability: Tools built for one environment don't easily move to another.
MCP solves this by introducing a client-server architecture where the server acts as a standardized gateway to the data, and the client (the LLM host) speaks a universal language to request information.
Understanding the MCP Architecture
The MCP ecosystem consists of three primary components:
- MCP Host: The application that initiates the LLM session (e.g., Claude Desktop, a custom IDE, or a dedicated AI agent platform).
- MCP Client: A component within the host that establishes a connection to the server.
- MCP Server: A lightweight service that exposes specific tools, resources, and prompts to the client.
The beauty of this setup is that the server can run locally on your machine or within your private VPC. The LLM never sees your raw database credentials; it only sees the standardized tool definitions and the specific data the server chooses to return.
Building an MCP Server with TypeScript
TypeScript is an ideal choice for building MCP servers due to its strong typing (crucial for defining tool schemas) and its vast ecosystem. Let’s build a practical example: an MCP server that allows an LLM to query an internal Customer Support API.
Step 1: Setting up the Project
First, initialize a new Node.js project and install the official MCP SDK:
mkdir support-mcp-server cd support-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node npx tsc --init
Step 2: Implementing the Server
We will create a server that exposes a tool called get_customer_tickets. This tool will fetch data from a hypothetical internal API.
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 { z } from "zod"; // 1. Initialize the Server const server = new Server( { name: "support-api-connector", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // 2. Define the available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_customer_tickets", description: "Retrieves support tickets for a specific customer by ID", inputSchema: { type: "object", properties: { customerId: { type: "string", description: "The unique ID of the customer" }, status: { type: "string", enum: ["open", "closed"], optional: true } }, required: ["customerId"], }, }, ], }; }); // 3. Implement the tool logic server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "get_customer_tickets") { const { customerId, status } = request.params.arguments as { customerId: string, status?: string }; // In a real scenario, this would be an authenticated fetch to your internal API const tickets = await fetchInternalTickets(customerId, status); return { content: [ { type: "text", text: JSON.stringify(tickets, null, 2), }, ], }; } throw new Error("Tool not found"); }); // 4. Start the server using STDIO transport const transport = new StdioServerTransport(); await server.connect(transport);
Why STDIO?
In the example above, we use StdioServerTransport. This is a common pattern for local MCP servers. The host (like Claude Desktop) starts the server as a child process and communicates with it via standard input/output. This is inherently secure because the server doesn't need to expose any network ports; it only exists as long as the host process is running.
Defining Robust Tools for LLM Consumption
The most critical part of an MCP server isn't the code—it's the description. LLMs use these descriptions to decide which tool to call.
When defining tools:
- Be Verbose: Instead of
description: "Gets tickets", usedescription: "Retrieves a list of active support tickets for a customer. Use this when a user asks about their recent issues or status updates." - Use Zod for Validation: While the MCP SDK uses JSON Schema for the
inputSchema, using Zod internally to validate theargumentsensures your logic doesn't crash on unexpected LLM hallucinations. - Handle Errors Gracefully: If the internal API is down, return a clear text message like
"Error: The support database is currently unreachable. Please try again in 5 minutes."This allows the LLM to explain the situation to the user rather than just failing.
Security and Data Privacy
MCP is a significant win for security-conscious organizations. Traditionally, to give an AI access to internal data, you might build a 'Retriever' in a RAG (Retrieval-Augmented Generation) pipeline. This often involves moving data into a third-party vector database.
With MCP, the data stays where it lives. The LLM only receives the specific slice of data it needs to answer a current prompt.
Key Security Patterns:
- Local Secrets: Credentials for your internal APIs are stored in your local environment variables or a secure vault, not in the LLM's system prompt or a cloud-hosted config.
- Read-Only by Default: When building MCP servers, start with read-only tools. Only implement 'write' tools (like
update_ticket_status) after implementing strict authentication and audit logging. - Human-in-the-loop: For sensitive actions, the MCP Host can be configured to require user approval before the client executes a specific tool call returned by the server.
Real-World Use Case: The Local Data Bridge
Imagine a data scientist working with sensitive healthcare data stored in local CSV files or a local PostgreSQL instance. They cannot upload this data to a cloud AI for analysis due to HIPAA compliance.
By writing a simple MCP server in TypeScript, the scientist can expose a query_local_db tool. The LLM can then write SQL queries, send them to the MCP server, and receive only the aggregated results (e.g., "The average patient age is 45"). The raw, sensitive records never leave the local machine.
// Example: Local DB Query Tool server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "query_local_db") { const { sql } = request.params.arguments as { sql: string }; // Sanitize and execute query locally const result = await db.all(sql); return { content: [{ type: "text", text: JSON.stringify(result) }], }; } // ... });
Best Practices for Agentic Reliability
To move from a 'cool demo' to a production-grade agent, follow these senior-level practices:
- Pagination: LLMs have context limits. If a tool might return 1,000 rows, implement pagination in your MCP tool so the agent can request
page 1, thenpage 2if needed. - Rate Limiting: Protect your internal APIs. The LLM might get into a loop calling a tool repeatedly. Implement basic throttling within your MCP server.
- Logging: Log every tool call and its arguments on the server side. This is your audit trail for understanding why an agent made a specific decision.
- Small, Atomic Tools: Instead of one
manage_everythingtool, create smaller tools likeget_user_email,list_user_orders, andrefund_order. This makes it easier for the LLM to select the right tool with high confidence.
Conclusion
The Model Context Protocol represents a shift toward a more modular and secure AI ecosystem. By standardizing the interface between models and data, it allows developers to focus on building value rather than fighting with integrations.
To get started with MCP today:
- Identify a Data Silo: Choose a local database or internal API that would benefit from AI interaction.
- Build a Server: Use the
@modelcontextprotocol/sdkto create a TypeScript server exposing that data. - Connect to a Host: Use Claude Desktop or the MCP Inspector to test your server.
- Iterate on Descriptions: Refine your tool descriptions based on how the LLM interacts with them.
By adopting MCP, you're not just building a better bot; you're building a future-proof architecture for the agentic era.