Save 10+ hours every week. Let AI run your busywork. Try Friday →
Save hours every week. Let AI handle the busywork. Try Friday →
how to build an ai agent with mcp
Building AI agents is now easier than ever with MCPs.

How to Build an AI Agent with Model Context Protocol (MCP)

Article Contents

Thursday is the New Friday

Friday AI does your busywork so fast, Thursday starts feeling like Friday afternoon. Especially you 🫵 product teams and web developers.

Get Friday

Over 1,000 MCP servers are now publicly available, and every major AI development platform, including Claude, ChatGPT, Cursor, and VS Code Copilot, has adopted the protocol. If you’re still wiring tools to your LLM with hand-rolled function definitions, you’re doing more work than you need to.

What MCP Actually Is

The Model Context Protocol (MCP) is an open standard created by Anthropic in November 2024. The pitch is simple: instead of writing a custom integration every time you want an LLM to talk to a database, a file system, or a third-party API, you write one MCP server and any MCP-compatible client can connect to it.

Think of it as a USB-C port for AI applications. The physical connector is standardized; what plugs into it is up to you.

MCP uses JSON-RPC 2.0 under the hood. A server exposes capabilities. A client (your AI app or an existing host like Claude Desktop) connects, discovers those capabilities, and invokes them. The LLM never calls your server directly. The host application acts as a broker: the model requests a tool call, the host sends that request to the MCP server, gets the result, and feeds it back to the model.

This separation matters more than it sounds. It means your server code is not LLM-specific. You write it once, and it works with any model or any client that speaks MCP. Anthropic, OpenAI, Google, and the major IDE vendors have all committed to the spec. That’s a rare instance of the industry actually converging on something.

The Three Primitives

Every MCP server exposes some combination of three capability types:

  • Tools: Functions the LLM can call (with user approval). These are the most commonly used primitive. A tool might query a database, send an HTTP request, or write to a file.
  • Resources: File-like data the client can read. Think of these as structured read-only endpoints: a configuration file, an API response, a document. The LLM uses them to load context rather than to take action.
  • Prompts: Pre-written templates that help users accomplish specific tasks. These show up as slash commands or context menus inside MCP host applications.

Most tutorials focus only on tools, and that’s fine for getting started. Resources become important once you’re building agents that need to load structured data without burning tokens on repeated tool calls. Prompts are mostly useful when you’re building an MCP server intended for end users rather than other developers.

Transport: stdio vs HTTP+SSE

MCP supports two transport mechanisms. Which one you choose depends on where your server runs.

stdio runs the server as a local subprocess. The client starts the process and communicates over stdin/stdout. This is what Claude Desktop uses. It’s simple, requires no network configuration, and is the right choice for developer tools, local file access, and anything that only needs to run on one machine. The critical rule: never write to stdout inside a stdio server. Even a single stray print() statement will corrupt the JSON-RPC stream and break the connection.

HTTP with SSE (Server-Sent Events) runs the server as a persistent HTTP service. Clients connect over the network. This is what you need for production deployments, multi-user agents, and any scenario where the server needs to live on a remote machine. The newer Streamable HTTP transport is replacing the older HTTP+SSE model, but most production SDKs support both.

Building a Weather MCP Server in Python

The official quickstart builds a weather server that wraps the US National Weather Service API. It’s a good starting point because it demonstrates the full structure without adding unnecessary complexity. Here’s a clean, annotated version.

First, set up the environment:

# Install uv (fast Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create and configure the project
uv init weather
cd weather
uv venv
source .venv/bin/activate

# Install the MCP SDK and httpx for API calls
uv add "mcp[cli]" httpx

Now create weather.py and add the server setup:

from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# FastMCP reads your type hints and docstrings to build tool definitions automatically
mcp = FastMCP("weather")

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with error handling."""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None

The FastMCP class is the high-level entry point for the Python SDK. It handles the protocol boilerplate and exposes a decorator-based API for registering tools. Under the hood it still speaks JSON-RPC, but you never touch that layer directly.

Now register your tools using the @mcp.tool() decorator:

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."

    if not data["features"]:
        return "No active alerts for this state."

    alerts = []
    for feature in data["features"]:
        props = feature["properties"]
        alerts.append(
            f"Event: {props.get('event', 'Unknown')}\n"
            f"Area: {props.get('areaDesc', 'Unknown')}\n"
            f"Severity: {props.get('severity', 'Unknown')}\n"
            f"Description: {props.get('description', 'No description available')}"
        )
    return "\n---\n".join(alerts)

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "Unable to fetch forecast data for this location."

    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:
        forecasts.append(
            f"{period['name']}:\n"
            f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
            f"Wind: {period['windSpeed']} {period['windDirection']}\n"
            f"Forecast: {period['detailedForecast']}"
        )
    return "\n---\n".join(forecasts)

def main():
    mcp.run(transport="stdio")

if __name__ == "__main__":
    main()

Notice the docstrings. MCP’s Python SDK reads them to build the tool descriptions the LLM receives. Write them carefully: they directly affect how accurately the model decides when and how to call the tool. Vague docstrings produce incorrect or unnecessary tool calls.

Start the server with:

uv run weather.py

The TypeScript Version

If you prefer TypeScript, the setup is similar but the API uses explicit method calls rather than decorators. This is actually useful for larger servers because the schema validation via Zod is more explicit.

# Initialize the project
mkdir weather && cd weather
npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D @types/node typescript

Create src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

const server = new McpServer({
  name: "weather",
  version: "1.0.0",
});

// Register a tool with explicit Zod schema validation
server.tool(
  "get_alerts",
  "Get weather alerts for a US state",
  {
    state: z.string().length(2).describe("Two-letter US state code (e.g. CA, NY)"),
  },
  async ({ state }) => {
    const url = `${NWS_API_BASE}/alerts/active/area/${state}`;
    try {
      const response = await fetch(url, {
        headers: { "User-Agent": USER_AGENT, Accept: "application/geo+json" },
      });
      const data = await response.json();

      if (!data.features?.length) {
        return { content: [{ type: "text", text: "No active alerts for this state." }] };
      }

      const alertText = data.features
        .map((f: any) => `Event: ${f.properties.event}\nArea: ${f.properties.areaDesc}`)
        .join("\n---\n");

      return { content: [{ type: "text", text: alertText }] };
    } catch {
      return { content: [{ type: "text", text: "Failed to fetch alerts." }] };
    }
  }
);

// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);

The TypeScript SDK requires you to return a { content: [...] } object from each tool handler. Each content item has a type (text, image, or resource) and a corresponding value. This explicit structure is more verbose than the Python version, but it gives you fine-grained control over what the model receives, including multimodal content.

The key difference worth noting: in TypeScript you pass the schema as a Zod object, which gives you runtime validation on top of type checking. In Python, FastMCP infers the schema from type annotations. Both approaches work, but the explicit Zod schema makes API contracts clearer in larger codebases.

Connecting to Claude Desktop

Once your server runs, connecting it to Claude Desktop or Claude Code takes a single config file edit. On macOS, open:

~/Library/Application Support/Claude/claude_desktop_config.json

Add your server under the mcpServers key:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/your/weather",
        "run",
        "weather.py"
      ]
    }
  }
}

On Windows, the config lives at %APPDATA%\Claude\claude_desktop_config.json. Use forward slashes or double-escaped backslashes in the path. Always use the absolute path to the server directory: relative paths break silently and are hard to debug.

Save the file and restart Claude Desktop. A hammer icon in the toolbar confirms the server connected successfully. You can now ask Claude questions that trigger your tools: “What are the weather alerts in California?” will call get_alerts with state="CA" and return the results directly in the conversation.

MCP vs the Alternatives

Before you commit to MCP for a project, it’s worth understanding where it fits relative to other approaches. The comparison below focuses on real tradeoffs, not feature checklists.

Feature / Factor MCP Raw Function Calling LangChain Tools Notes
Client portability Any MCP host LLM-specific LangChain stack only MCP servers work across Claude, ChatGPT, Cursor, VS Code Copilot
Setup complexity Medium Low High Raw function calling is the simplest for a single-LLM project
Schema definition Auto (Python) / Zod (TS) Manual JSON schema Pydantic / decorator All three require you to describe tool arguments precisely
Multi-agent support Yes (MCP client role) Custom Yes (agent chains) MCP servers can act as clients to other servers
Transport options stdio, HTTP+SSE In-process In-process / HTTP MCP’s stdio mode is well-suited for local dev tooling
Ecosystem maturity Growing fast Mature Mature but fragmented 1,000+ public MCP servers as of mid-2026
Production-ready Yes (HTTP transport) Yes Yes MCP’s Streamable HTTP transport is now the recommended production path

Where MCP Is Worth It, and Where It Isn’t

MCP adds a layer. That layer has a cost: a subprocess to manage, a config file to maintain, a protocol to debug when things go wrong. For a quick proof of concept using a single LLM and a couple of function definitions, raw function calling is less friction. MCP pays off when one or more of these conditions are true:

  • You’re building a tool that multiple AI clients should be able to use. Writing one MCP server beats maintaining four separate integrations.
  • Your team includes non-ML engineers who need to expose internal APIs to an AI system without learning the specifics of each model’s function-calling API.
  • You’re targeting Claude Desktop, Cursor, VS Code Copilot, or any other MCP host, and you want the tools to appear natively in those environments.
  • You need the Resources primitive to stream structured data into a model’s context without repeated tool calls.

Where MCP is overkill: single-model prototypes with two or three tools, applications where you control the entire stack and cross-client portability has no value, and cases where the overhead of running a subprocess adds unacceptable latency.

The stdio transport is particularly well-suited to developer tools that extend Claude’s Dynamic Workflows, which can run hundreds of parallel subagents in a single session. Each subagent can connect to the same MCP server, meaning your server handles context access for an entire swarm with one codebase. That use case didn’t exist eighteen months ago, and it changes the calculus significantly.

Extending Beyond the Basics

The weather server is a clean minimal example, but production MCP servers tend to need a few more patterns. Here’s what comes up most often:

  1. Authentication: For HTTP-transport servers, add an auth layer at the HTTP level (Bearer token, OAuth). The MCP spec doesn’t mandate a particular auth mechanism; you handle it in your HTTP middleware before the MCP layer sees the request.
  2. Stateful servers: stdio servers are per-connection. If you need shared state across multiple client sessions, use the HTTP transport and manage state in your server process or a backing store.
  3. Resources for large data: When a tool would return a large blob repeatedly, consider exposing it as a Resource instead. Clients can cache resources and only fetch updates when the content changes, reducing redundant API calls and token usage.
  4. Error handling: Always return a meaningful string from tool failures rather than raising an exception. The LLM handles a returned error message far better than a connection drop or a JSON-RPC error code it wasn’t expecting.

The SDK repositories for Python (pip install mcp) and TypeScript (npm install @modelcontextprotocol/sdk) are actively maintained and ship with a CLI inspector: mcp dev weather.py opens a local debugging interface where you can call your tools directly without a host client. Use it before wiring up Claude Desktop. It saves significant debugging time.

If you’re building agents that consume third-party APIs alongside your own server, check the official open-source server repository. Pre-built servers for GitHub, Postgres, Google Drive, Slack, and Puppeteer are available and production-tested. Wrapping one is often faster than writing from scratch.

For developers using the Vercel AI SDK, MCP client support is built in as of the recent agentic release. You can connect existing MCP servers to Vercel AI SDK agents with a few lines of configuration, which makes MCP a natural fit for Next.js-based AI applications.

The Bottom Line

Building an AI agent with the Model Context Protocol means writing a server once and getting compatibility with every major AI client for free. The protocol is simple: three primitives, two transports, one config file to connect everything. The Python SDK reduces boilerplate to near zero with FastMCP, and the TypeScript SDK gives you explicit schema validation with Zod. The real value isn’t in any individual feature. It’s in the fact that the industry, unusually, agreed on a standard before the ecosystem fragmented beyond repair. MCP’s adoption will only accelerate as agentic frameworks push developers toward systems where dozens of specialized servers collaborate. Start with a small tool server, get it running in Claude Desktop, and you’ll understand the pattern well enough to build anything more complex on top of it.

AI_INIT(); WHILE (IDE_OPEN) { VIBE_CHECK(); PROMPT_TO_PROFIT(); SHIP_IT(); } // 100% SUCCESS_RATE // NO_DEBT_FOUND

Your FreeVibe Coding Manual_

Join Bind AI’s Vibe Coding Course to learn vibe coding fundamentals, ship real apps, and convert it from a hobby to a profession. Learn the math behind web development, build real-world projects, and get 50 IDE credits.

ENROLL FOR FREE _
No credit Card Required | Beginner Friendly

Build whatever you want, however you want, with Bind AI.

Clone your developer

Friday AI is the only desktop-native coworker that:

🟢 Watches your screen to understand your UI and app architecture.
🟢 Learns your workflow from dev server to deployment.
🟢 Actually hits ‘Submit’ to push your code and ship features.

Integrate your entire stack and build full-scale applications while you’re still on your first cup of coffee.

Get 100 credits for free upon sign-up!