{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chapter 10 \u00b7 Build Your First MCP Server\n",
    "\n",
    "### MCP is a universal port for AI\n",
    "\n",
    "Rewind to before USB. Every gadget shipped with its own oddball plug, and half your\n",
    "desk was a drawer of adapters. Then one standard port arrived and everything just\n",
    "worked together.\n",
    "\n",
    "**MCP \u2014 the Model Context Protocol \u2014 is that universal port, but for AI.** It's an open\n",
    "standard for connecting models to the outside world: tools, data, and services. Instead\n",
    "of hand-wiring every model to every service (the N x M tangle), you build a connector\n",
    "**once** and any MCP-compatible model can plug into it.\n",
    "\n",
    "### What you'll do in this notebook\n",
    "\n",
    "1. **Write a tiny MCP server** \u2014 a real, working program that exposes two *tools*.\n",
    "2. **Connect a client to it**, ask it what tools it offers, and call them.\n",
    "3. See the payoff: the same server would work with *any* MCP client \u2014 no custom glue.\n",
    "\n",
    "By the end you'll have felt the core idea with your own hands: **build once, any model can use it.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **An honest note before we start.** MCP normally shines when it connects *separate*\n",
    "> apps and models \u2014 a desktop assistant reaching into Slack, an IDE reaching into your\n",
    "> database. Here we run the client **and** the server inside one notebook. That's not the\n",
    "> real-world deployment shape \u2014 it's a lab bench. We put both halves on the same table so\n",
    "> you can watch the mechanism work end to end. The protocol is identical; only the distance\n",
    "> between the two ends changed."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 \u00b7 Setup\n",
    "\n",
    "One install. The `mcp` package gives us both halves we need: `FastMCP` to *write* a\n",
    "server, and a client to *talk* to one. No API key required for this notebook \u2014 we're\n",
    "exercising the plumbing, not calling an LLM."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "!pip install -q mcp"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 \u00b7 Write a tiny MCP server\n",
    "\n",
    "An MCP server is smaller than people expect. With the `FastMCP` helper you take a plain\n",
    "Python function, decorate it with `@mcp.tool()`, and give it a **docstring** \u2014 that\n",
    "docstring *is* the interface. It's how a model discovers what the tool does and when to\n",
    "reach for it.\n",
    "\n",
    "We'll expose a mini team directory with two tools:\n",
    "\n",
    "- `get_member_role(name)` \u2014 look up one person's role\n",
    "- `list_members()` \u2014 list everyone\n",
    "\n",
    "The `%%writefile` magic saves the cell to a file called `team_server.py` instead of\n",
    "running it. We want the server as a standalone program so the client can launch it as a\n",
    "separate process \u2014 exactly how MCP works in the real world."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "%%writefile team_server.py\n",
    "from mcp.server.fastmcp import FastMCP\n",
    "\n",
    "mcp = FastMCP(\"team\")\n",
    "\n",
    "MEMBERS = {\"Maya\": \"Team Lead\", \"Arjun\": \"Engineer\", \"Lena\": \"Designer\"}\n",
    "\n",
    "@mcp.tool()\n",
    "def get_member_role(name: str) -> str:\n",
    "    \"\"\"Return the role of a team member by name.\"\"\"\n",
    "    return MEMBERS.get(name, \"Unknown member\")\n",
    "\n",
    "@mcp.tool()\n",
    "def list_members() -> list[str]:\n",
    "    \"\"\"List all team members.\"\"\"\n",
    "    return list(MEMBERS.keys())\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    mcp.run(transport=\"stdio\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's a complete, real MCP server. A few things to notice:\n",
    "\n",
    "- **`@mcp.tool()`** turns an ordinary function into a *discoverable capability*.\n",
    "- **The docstring is not decoration** \u2014 it's the description the client hands to a model\n",
    "  so it knows what the tool is for.\n",
    "- **`transport=\"stdio\"`** means the server talks over standard input/output. That's the\n",
    "  channel a client uses when it launches the server as a subprocess (what we'll do next)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 \u00b7 Connect a client, list its tools, call them\n",
    "\n",
    "Now the other half. We'll act as an **MCP client**: launch the server, open a session,\n",
    "and ask it \u2014 over the standard protocol \u2014 *what can you do?* Then we'll call the tools.\n",
    "\n",
    "The flow, step by step:\n",
    "\n",
    "1. `StdioServerParameters` says *how* to start the server (`python team_server.py`).\n",
    "2. `stdio_client(...)` launches it and hands us a read/write pipe.\n",
    "3. `ClientSession` speaks MCP over that pipe. `initialize()` does the handshake.\n",
    "4. `list_tools()` \u2014 the client **discovers** the tools automatically. We never told it\n",
    "   their names; the server announced them.\n",
    "5. `call_tool(name, args)` runs one and returns the result.\n",
    "\n",
    "MCP is asynchronous, so we use `async`/`await`. Colab supports top-level `await`, and\n",
    "`nest_asyncio` is applied defensively so the event loop plays nicely inside the notebook."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "import nest_asyncio; nest_asyncio.apply()\n",
    "\n",
    "params = StdioServerParameters(command=\"python\", args=[\"team_server.py\"])\n",
    "\n",
    "async def run():\n",
    "    async with stdio_client(params) as (read, write):\n",
    "        async with ClientSession(read, write) as session:\n",
    "            await session.initialize()\n",
    "\n",
    "            # 1) Discover what the server offers \u2014 we never hard-coded these names.\n",
    "            tools = await session.list_tools()\n",
    "            print(\"Tools the server exposes:\", [t.name for t in tools.tools])\n",
    "\n",
    "            # 2) Call a tool with an argument.\n",
    "            res = await session.call_tool(\"get_member_role\", {\"name\": \"Maya\"})\n",
    "            print(\"get_member_role('Maya') ->\", res.content[0].text)\n",
    "\n",
    "            # 3) Call a tool with no arguments.\n",
    "            res2 = await session.call_tool(\"list_members\", {})\n",
    "            print(\"list_members() ->\", res2.content[0].text)\n",
    "\n",
    "await run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should see the two tool names printed, then Maya's role (`Team Lead`), then the full\n",
    "member list. If you do \u2014 congratulations, you just ran a full MCP round trip: a client\n",
    "discovered and called tools on a server it had never seen the code for."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4 \u00b7 What just happened \u2014 and why it's reusable\n",
    "\n",
    "Trace what the client actually knew: **nothing, at first.** It didn't import\n",
    "`get_member_role`. It didn't know the team had three people. It launched the server,\n",
    "asked *\"what tools do you have?\"*, and the server answered over the protocol. Only then\n",
    "did the client call anything.\n",
    "\n",
    "That indirection is the whole point:\n",
    "\n",
    "- The server exposed **tools** over a **standard protocol** \u2014 not a private, one-off API.\n",
    "- **Any** MCP-compatible client or model could do exactly what we just did, with **zero**\n",
    "  custom glue code. Swap our client for Claude Desktop, an IDE, or your own app \u2014 the\n",
    "  server doesn't change.\n",
    "\n",
    "Contrast that with the world *without* MCP. If you have **N** models and **M** services,\n",
    "and every model needs a bespoke integration for every service, that's **N x M** custom\n",
    "connectors \u2014 none reusable. Add one service and you rewrite it for every model.\n",
    "\n",
    "MCP turns that **N x M tangle** into **N + M**: each model speaks MCP once, each service\n",
    "exposes an MCP server once, and they all snap together. That's why the moment MCP landed,\n",
    "people published servers for GitHub, Slack, Notion, Postgres, filesystems \u2014 each one\n",
    "instantly usable by every client in the ecosystem. **Build once, use everywhere.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5 \u00b7 Stretch idea (no code) \u2014 hand these tools to an LLM agent\n",
    "\n",
    "In this notebook *we* decided when to call `get_member_role`. In a real agent, the\n",
    "**model** makes that decision. Here's how you'd wire it up, connecting back to the\n",
    "tool-use pattern from Chapter 5:\n",
    "\n",
    "1. **Connect and discover.** Open the MCP session and `list_tools()` \u2014 just like above.\n",
    "2. **Translate to the model's tool format.** Each MCP tool has a name, a description\n",
    "   (that docstring again), and an input schema. Convert that into the function-declaration\n",
    "   format a model like Gemini expects, and pass the list into the chat request.\n",
    "3. **Let the model choose.** You send a user message \u2014 *\"What does Maya do on the team?\"*\n",
    "   \u2014 and instead of answering directly, the model replies with a **tool call**:\n",
    "   `get_member_role(name=\"Maya\")`.\n",
    "4. **You execute it via MCP.** Your code catches that request and runs\n",
    "   `session.call_tool(\"get_member_role\", {\"name\": \"Maya\"})` \u2014 the exact call we made by\n",
    "   hand.\n",
    "5. **Feed the result back.** Return `\"Team Lead\"` to the model, which folds it into a\n",
    "   natural-language answer for the user.\n",
    "\n",
    "The beautiful part: **you never wrote a Maya-specific integration for Gemini.** The MCP\n",
    "server exposed the tool; the agent loop discovered it and decided when to use it. Point\n",
    "the same loop at a Slack MCP server tomorrow and the agent gains Slack \u2014 no new glue."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6 \u00b7 \ud83c\udfaf Your turn\n",
    "\n",
    "Time to extend the server yourself. Add a **third tool** \u2014 `get_member_email` \u2014 that\n",
    "returns a team member's email, then discover and call it from the client.\n",
    "\n",
    "**Steps:**\n",
    "\n",
    "1. In the server file, add an `EMAILS` dictionary (e.g. `\"Maya\": \"maya@team.dev\"`).\n",
    "2. Add a new `@mcp.tool()` function `get_member_email(name: str) -> str` with a clear\n",
    "   docstring \u2014 remember, the docstring is the interface.\n",
    "3. Re-run the client cell and confirm `get_member_email` now appears in the tools list.\n",
    "4. Call it and print the result.\n",
    "\n",
    "The starter below is the full server again with a `TODO`. Fill it in, run this cell to\n",
    "overwrite `team_server.py`, then re-run your client from Section 3.\n",
    "\n",
    "*Tip: the client rediscovers tools every time it connects \u2014 you don't touch the client\n",
    "code at all. That's the reusability you just read about, working in your favor.*"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "%%writefile team_server.py\n",
    "from mcp.server.fastmcp import FastMCP\n",
    "\n",
    "mcp = FastMCP(\"team\")\n",
    "\n",
    "MEMBERS = {\"Maya\": \"Team Lead\", \"Arjun\": \"Engineer\", \"Lena\": \"Designer\"}\n",
    "EMAILS = {\"Maya\": \"maya@team.dev\", \"Arjun\": \"arjun@team.dev\", \"Lena\": \"lena@team.dev\"}\n",
    "\n",
    "@mcp.tool()\n",
    "def get_member_role(name: str) -> str:\n",
    "    \"\"\"Return the role of a team member by name.\"\"\"\n",
    "    return MEMBERS.get(name, \"Unknown member\")\n",
    "\n",
    "@mcp.tool()\n",
    "def list_members() -> list[str]:\n",
    "    \"\"\"List all team members.\"\"\"\n",
    "    return list(MEMBERS.keys())\n",
    "\n",
    "# TODO: add a third tool below.\n",
    "# @mcp.tool()\n",
    "# def get_member_email(name: str) -> str:\n",
    "#     \"\"\"Return the email address of a team member by name.\"\"\"\n",
    "#     return EMAILS.get(name, \"Unknown member\")\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    mcp.run(transport=\"stdio\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once `get_member_email` is live, add this to your client's `run()` and re-run Section 3:\n",
    "\n",
    "```python\n",
    "res3 = await session.call_tool(\"get_member_email\", {\"name\": \"Arjun\"})\n",
    "print(\"get_member_email('Arjun') ->\", res3.content[0].text)\n",
    "```\n",
    "\n",
    "If it prints `arjun@team.dev`, you've shipped a new capability to *every* MCP client in\n",
    "one small edit \u2014 and never touched the client. That's the USB moment.\n",
    "\n",
    "---\n",
    "\n",
    "*Part of the Agentic AI course \u00b7 Chapter 10*"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "name": "ch10-mcp-server.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}