# Output the tool call generated by the model
print(response.choices[0].message.tool_calls)
```

However, using these toolchains comes with some issues:

- You are limited to a single model provider.
- Prompts also remain brittle, undocumented, and tied to proprietary formats.

This is where the Model Context Protocol (MCP) comes in.

## What is MCP?

MCP is a low-level JSON-RPC protocol originally proposed by Anthropic. It standardizes communication between LLMs and real-world environments.

At its core, MCP defines two roles: the server and the client.

The **MCP server** is the backend that exposes three primitives:

- Tools: Functions that perform actions (such as writing a file or placing an order)
- Resources: Read-only, URI-addressable context (for example, logs, config, and APIs)
- Prompts: Reusable message templates that guide interactions with the MCP server

The **MCP client** is the component that understands the protocol and communicates with the MCP server by sending the following requests and interpreting the server's structured responses:

- `tools/call`
- `resources/read`
- `prompts/get`

Here's where confusion often arises: Who actually talks to the MCP server? Is it the LLM or the agent?

The answer is that neither the LLM nor the agent talks directly to the MCP server.

The MCP client acts as the middle layer between the server and the model. It may be embedded in:

- A desktop application (such as Claude Desktop)
- An agent runtime
- A custom LLM client

These systems use the MCP client to interact with the MCP server, but they are still responsible for integrating results into LLM prompts or workflows.

For example, in an agentic architecture, the flow looks like this:

1. The user inputs, _"How many users signed up this month?"_
2. The agent parses the request and calls a tool.
3. The agent's embedded MCP client sends a `tools/call` request to the MCP server.
4. The MCP server executes the tool function, which makes a database query and returns a result. The MCP server returns the result with a structured response to the MCP client.
5. The agent takes that response and incorporates it into the next LLM prompt or uses it to make a decision.

## How does MCP differ from APIs and tool calling?

Unlike many client-server protocols where clients only make requests and servers only respond, MCP supports full bidirectional communication.

- **Client-to-server**: The client can request resources, call tools, and get prompts.
- **Server-to-client**: The server can request LLM sampling, ask for root listings, and send notifications.
- **Notifications**: Both sides can send one-way messages without expecting responses.

This bidirectional design enables workflows in which servers can actively participate in decision-making by requesting LLM assistance through the client.

![MCP Architecture](/assets/mcp/getting-started/mcp-architecture.png)

MCP offers SDKs in multiple languages (including Python and JavaScript) to simplify building and exposing MCP-compliant servers.

## MCP Server Example

Here is an example of an MCP server in Python that exposes a single tool to write notes to a local directory:

```python filename="mcp-server.py"
from mcp.server.fastmcp import FastMCP
from mcp.server.stdio import stdio_server
from mcp.server import InitializationOptions, NotificationOptions
from pathlib import Path
import asyncio

NOTES_DIR = Path("notes")
NOTES_DIR.mkdir(exist_ok=True)

app = FastMCP("MCP Notes Server")

#### TOOLS ####
@app.tool()
def write_note(slug: str, content: str) -> str:
  note_path = NOTES_DIR / f"{slug}.txt"
  note_path.write_text(content.strip(), encoding="utf-8")
  return f"Note '{slug}' saved."

#### RESOURCES ####
@app.resource("note://{slug}")
def read_note(slug: str) -> str:
  note_path = NOTES_DIR / f"{slug}.txt"
  if not note_path.exists():
      return "Note not found."
  return note_path.read_text(encoding="utf-8")

#### PROMPTS ####
@app.prompt()
def suggest_note_prompt(topic: str) -> str:
  return f"""
      Write a short, thoughtful note for someone who needs advice about: {topic}
  """

#### ROOTS ####
@app.tool()
def list_notes(root: str = None) -> list[str]:
  all_slugs = [f"note://{f.stem}" for f in NOTES_DIR.glob("*.txt")]
  if root:
      return [slug for slug in all_slugs if slug.startswith(root)]
  return all_slugs