Tekko

Language

Get in Touch →

Usually respond within 24 hours

Back to BlogAI & ML

Building Secure Agentic Workflows with MCP and TypeScript

8 min read
MCPTypeScriptAnthropicLLMArchitecture
Building Secure Agentic Workflows with MCP and TypeScript

The fundamental challenge in building production-grade AI agents isn't the model's reasoning capability—it's the data gap. Large Language Models (LLMs) are trained on vast public datasets, but they lack visibility into your private codebase, your internal APIs, and your local databases.

Until recently, developers bridged this gap using bespoke integrations, complex RAG (Retrieval-Augmented Generation) pipelines, or custom-coded 'function calling' wrappers. This approach is brittle, hard to scale, and often poses significant security risks. Anthropic’s Model Context Protocol (MCP) changes this dynamic by introducing an open standard for how AI models interact with data sources and tools.

As a senior engineer, you need to understand not just how to prompt a model, but how to build the infrastructure that allows it to act as a secure, useful agent within your ecosystem. In this article, we’ll explore how to implement MCP using TypeScript to connect LLMs to local data sources and internal APIs safely.

The MCP Paradigm: Moving Beyond Simple Chat

To understand MCP, think of it as the Language Server Protocol (LSP) but for AI. Just as LSP allowed IDEs like VS Code to support dozens of programming languages through a standardized interface, MCP allows any AI 'Host' (like Claude Desktop or a custom IDE) to interact with any 'Server' (your data source) without needing a unique integration for every pair.

The Three Pillars of MCP

  1. Hosts: The application where the LLM resides (e.g., Claude Desktop, IDEs, or your custom-built AI app).
  2. Clients: The component within the host that initiates the connection to the server.
  3. Servers: Lightweight programs that expose specific capabilities (Tools, Resources, or Prompts) to the client.

By decoupling the model from the data source, MCP allows you to maintain a strict security boundary. The model never 'sees' your database credentials; it only sees the schema and the tools you explicitly choose to expose.

Setting Up an MCP Server with TypeScript

TypeScript is the ideal language for building MCP servers due to its strong typing and the excellent SDK support provided by Anthropic. Let’s walk through building a server that connects an LLM to a local SQLite database—a common requirement for developers needing to query internal logs or project metadata.

1. Project Initialization

First, initialize a new Node.js project and install the necessary dependencies:

mkdir mcp-data-server && cd mcp-data-server npm init -y npm install @modelcontextprotocol/sdk sqlite3 zod npm install -D @types/node @types/sqlite3 typescript ts-node

2. Defining the Server Logic

In MCP, a Tool is an executable function that the LLM can call. We use zod to define strict input schemas, ensuring the LLM doesn't pass malformed data to our internal systems.

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"; import sqlite3 from "sqlite3"; // Initialize the database connection const db = new sqlite3.Database("./internal_data.db"); const server = new Server({ name: "internal-data-explorer", version: "1.0.0", }, { capabilities: { tools: {}, }, }); // Define the tools available to the LLM server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "query_database", description: "Execute a read-only SQL query on the internal metrics database", inputSchema: { type: "object", properties: { sql: { type: "string", description: "The SQL query to run" }, }, required: ["sql"], }, }, ], }));

3. Implementing the Tool Execution

The actual execution of the tool must be handled with care. We use the CallToolRequestSchema to process incoming requests from the LLM.

server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "query_database") { const { sql } = request.params.arguments as { sql: string }; // Security Check: Only allow SELECT statements if (!sql.trim().toLowerCase().startsWith("select")) { return { content: [{ type: "text", text: "Error: Only read-only SELECT queries are allowed." }], isError: true, }; } return new Promise((resolve) => { db.all(sql, [], (err, rows) => { if (err) { resolve({ content: [{ type: "text", text: `Database error: ${err.message}` }], isError: true, }); } else { resolve({ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }], }); } }); }); } throw new Error("Tool not found"); }); // Start the server using Standard Input/Output as the transport const transport = new StdioServerTransport(); await server.connect(transport);

Agentic Tool-Use: The "Thought Loop"

When an LLM uses an MCP tool, it doesn't just execute a command and return a result. It enters an agentic loop. For example, if a user asks, "Which project had the most commits last week?", the process looks like this:

  1. Reasoning: The LLM realizes it doesn't have this data. It looks at its available MCP tools.
  2. Tool Call: It selects query_database and constructs a SQL query based on the schema you provided.
  3. Execution: Your TypeScript server runs the query and returns the JSON result.
  4. Observation: The LLM parses the JSON. If the data is incomplete, it might call another tool or refine its query.
  5. Response: The LLM provides a natural language answer to the user.

This "thought loop" is what makes MCP powerful. The LLM acts as a reasoning engine over your data, rather than just a text generator.

Security Considerations for Local Data

Connecting an LLM to internal APIs and local files introduces unique security challenges. As a senior engineer, you must implement multiple layers of defense.

Input Validation and Sanitization

Never trust the LLM's output. In the example above, we checked if the SQL query started with SELECT. In a production environment, you should use a restricted database user with read-only permissions or, better yet, use an ORM to prevent SQL injection and limit the LLM to specific, pre-defined query functions.

The Principle of Least Privilege

Only expose the specific tools and resources necessary for the task. If an agent only needs to read logs, don't give it a tool that can delete files. MCP allows you to define granular capabilities at the server level.

Human-in-the-Loop (HITL)

For sensitive actions—like sending an email, merging code, or updating a production database—the MCP server should not execute the action immediately. Instead, it should return a "proposal" that requires a human to click "Approve" in the Host application.

Transport Security

While many MCP implementations use StdioServerTransport (where the host and server communicate via stdin/stdout), remote MCP implementations use SSE (Server-Sent Events). In those cases, ensure you are using robust authentication (like API keys or OAuth2) and TLS encryption.

Connecting to Internal APIs

MCP isn't limited to local databases. It’s an excellent way to wrap internal microservices. Instead of writing a custom UI for every internal tool, you can build an MCP server that acts as a gateway to your internal API ecosystem.

Consider an MCP tool that interacts with an internal HR API:

server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "get_employee_benefits") { const { employeeId } = request.params.arguments as { employeeId: string }; const response = await fetch(`https://api.internal.company.com/v1/benefits/${employeeId}`, { headers: { "Authorization": `Bearer ${process.env.INTERNAL_API_KEY}` } }); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; } });

In this scenario, the LLM never sees the INTERNAL_API_KEY. The key stays securely on your server, and the LLM only interacts with the high-level tool interface.

Operational Best Practices

To ensure your MCP implementation is maintainable and robust, follow these guidelines:

  • Semantic Versioning: Use SemVer for your MCP servers. As you update tool schemas, ensure you don't break existing agent workflows.
  • Comprehensive Descriptions: The LLM relies on the description field in the tool definition to understand when and how to use a tool. Be verbose and provide examples within the description.
  • Logging and Telemetry: Log every tool call made by the LLM. This is vital for debugging "hallucinations" and for auditing security events.
  • Error Gracefulness: If a tool fails, return a descriptive error message to the LLM. This allows the model to attempt a correction (e.g., "The table 'users' does not exist; did you mean 'user_accounts'?").

Conclusion: The Future of Context-Aware AI

The Model Context Protocol represents a shift in how we build AI applications. By moving away from monolithic, "all-knowing" models and toward a modular architecture of specialized MCP servers, we create AI systems that are more secure, more accurate, and easier to maintain.

For the senior engineer, the path forward is clear:

  1. Audit your internal data sources and identify which would benefit from AI-assisted reasoning.
  2. Build small, focused MCP servers using TypeScript to expose these sources as Tools or Resources.
  3. Enforce strict security boundaries through input validation and the principle of least privilege.
  4. Iterate on tool descriptions to optimize the agentic loop.

By implementing MCP, you aren't just building a chatbot; you're building a secure, extensible platform that allows AI to become a truly integrated member of your technical stack.