Atualize para o Pro

TOWARDSDATASCIENCE.COM
How I Finally Understood MCP — and Got It Working in Real Life
Table of Content Introduction: Why I Wrote This The Evolution of Tool Integration with LLMs What Is Model Context Protocol (MCP), Really? Wait, MCP sounds like RAG… but is it? In an MCP-based setup In a traditional RAG system Traditional RAG Implementation MCP Implementation Quick recap! Core Capabilities of an MCP Server Real-World Example: Claude Desktop + MCP (Pre-built Servers) Build Your Own: Custom MCP Server from Scratch Congrats, You’ve Mastered MCP! References Introduction: Why I Wrote This I will be honest. When I first saw the term “Model Context Protocol (mcp),” I did what most developers do when confronted with yet another new acronym: I skimmed a tutorial, saw some JSON, and quietly moved on. “Too abstract,” I thought. Fast-forward to when I actually tried to integrate some custom tools with Claude Desktop— something that needed memory or access to external tools — and suddenly, MCP wasn’t just relevant. It was essential. The problem? None of the tutorials I came across felt beginner-friendly. Most jumped straight into building a custom MCP server without explaining in details why you’d need a server in the first place — let alone mentioning that prebuilt MCP servers already exist and work out of the box. So, I decided to learn it from the ground up. I read everything I could, experimented with both prebuilt and custom servers, integrated it with Claude Desktop and tested whether I could explain it to my friends —people with zero prior context. When I finally got the nod from them, I knew I could break it down for anyone, even if you’ve never heard of MCP until five minutes ago. This article breaks down what MCP is, why it matters, and how it compares to other popular architectures like RAG. We’ll go from “what even is this?” to spinning up your own working Claude integration — no prior MCP knowledge required. If you’ve ever struggled to get your AI model to feel a little less like a goldfish, this is for you. The Evolution of Tool Integration with LLMs Before diving into MCP, let’s understand the progression of how we connect Large Language Models (LLMs) to external tools and data: Image by author Standalone LLMs: Initially, models like GPT and Claude operated in isolation, relying solely on their training data. They couldn’t access real-time information or interact with external systems. Tool Binding: As LLMs advanced, developers created methods to “bind” tools directly to models. For example, with LangChain or similar frameworks, you could do something like: llm = ChatAnthropic() augmented_llm = llm.bind_tools([search_tool, calculator_tool]) This works well for individual scripts but doesn’t scale easily across applications. Why? Because tool binding in frameworks like LangChain is typically designed around single-session, stateless interactions, meaning every time you spin up a new agent or function call, you’re often re-defining which tools it can access. There’s no centralized way to manage tools across multiple interfaces or user contexts. 3. Application Integration Challenge: The real complexity arises when you want to integrate tools with AI-powered applications like IDEs (Cursor, VS Code), chat interfaces (Claude Desktop), or other productivity tools. Each application would need custom connectors for every possible tool or data source, creating a tangled web of integrations. This is where MCP enters the picture — providing a standardized layer of abstraction for connecting AI applications to external tools and data sources. What Is Model Context Protocol (MCP), Really? Let’s break it down: Model: The LLM at the heart of your application — GPT, Claude, whatever. It’s a powerful reasoning engine but limited by what it was trained on and how much context it can hold. Context: The extra information your model needs to do its job — documents, search results, user preferences, recent history. Context extends the model’s capabilities beyond its training set. Protocol: A standardized way of communicating between components. Think of it as a common language that lets your model interact with tools and data sources in a predictable way. Put those three together, and MCP becomes a framework that connects models to contextual information and tools through a consistent, modular, and interoperable interface. Much like HTTP enabled the web by standardizing how browsers talk to servers, MCP standardizes how AI applications interact with external data and capabilities. Pro tip! An easy way to visualize MCP is to think of it like tool binding for the entire AI stack, not just a single agent. That’s why Anthropic describes MCP as “a USB-C port for AI applications.” Image by author, inspired by Understanding MCP From Scratch by LangChain Wait, MCP sounds like RAG… but is it? A lot of people ask, “How is this different from RAG?” Great question. At a glance, both MCP and RAG aim to solve the same problem: give language models access to relevant, external information. But how they do it — and how maintainable they are — differs significantly. In an MCP-based setup Your AI app (host/client) connects to an MCP document server You interact with context using a standardized protocol You can add new documents or tools without modifying the app Everything works via the same interface, consistently Image by author, inspired by MCP Documentation. In a traditional RAG system Your app manually builds and queries a vector database You often need custom embedding logic, retrievers, and loaders Adding new sources means rewriting part of your app code Every integration is bespoke, tightly coupled to your app logic The key distinction is abstraction: The Protocol in Model Context Protocol is nothing but a standardized abstraction layer that defines bidirectional communication between MCP Client/Host and MCP Servers. Image by author, inspired by MCP Documentation. MCP gives your app the ability to ask, “Give me information about X,” without knowing how that info is stored or retrieved. RAG systems require your app to manage all of that. With MCP, your application logic stays the same, even as your document sources evolve. Let’s look at some high-level codes to see how these approaches differ: Traditional RAG Implementation In a traditional RAG implementation, your application code directly manages connections to document sources: # Hardcoded vector store logic vectorstore = FAISS.load_local("store/embeddings") retriever = vectorstore.as_retriever() response = retriever.invoke("query about LangGraph") With tool binding, you define tools and bind them to an LLM, but still need to modify the tool implementation to incorporate new data sources. You still need to update the tool implementation when your backend changes. @tool def search_docs(query: str): return search_vector_store(query) MCP Implementation With MCP, your application connects to a standardized interface, and the server handles the specifics of document sources: # MCP Client/Host: Client/Host stays the same # MCP Server: Define your MCP server # Import necessary libraries from typing import Any from mcp.server.fastmcp import FastMCP # Initialize FastMCP server mcp = FastMCP("your-server") # Implement your server's tools @mcp.tool() async def example_tool(param1: str, param2: int) -> str: """An example tool that demonstrates MCP functionality. Args: param1: First parameter description param2: Second parameter description Returns: A string result from the tool execution """ # Tool implementation result = f"Processed {param1} with value {param2}" return result # Example of adding a resource (optional) @mcp.resource() async def get_example_resource() -> bytes: """Provides example data as a resource. Returns: Binary data that can be read by clients """ return b"Example resource data" # Example of adding a prompt template (optional) mcp.add_prompt( "example-prompt", "This is a template for {{purpose}}. You can use it to {{action}}." ) # Run the server if __name__ == "__main__": mcp.run(transport="stdio") Then, you configure the host or client (like Claude Desktop) to use the server by updating its configuration file. { "mcpServers": { "your-server": { "command": "uv", "args": [ "--directory", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/your-server", "run", "your-server.py" ] } } } If you change where or how the resources/documents are stored, you update the server — not the client. That’s the magic of abstraction. And for many use cases — especially in production environments like IDE extensions or commercial applications — you can’t touch the client code at all. MCP’s decoupling is more than just a nice-to-have: it’s a necessity. It isolates the application code so that only the server-side logic (tools, data sources, or embeddings) needs to evolve. The host application remains untouched. This enables rapid iteration and experimentation without risking regression or violating application constraints. Quick recap! Hopefully, by now, it’s clear why MCP actually matters. Imagine you’re building an AI assistant that needs to: Tap into a knowledge base Execute code or scripts Keep track of past user conversations Without MCP, you’re stuck writing custom glue code for every single integration. Sure, it works — until it doesn’t. It’s fragile, messy, and a nightmare to maintain at scale. MCP fixes this by acting as a universal adapter between your model and the outside world. You can plug in new tools or data sources without rewriting your model logic. That means faster iteration, cleaner code, fewer bugs, and AI applications that are actually modular and maintainable. And I hope you were paying attention when I said MCP enables bidirectional communication between the host (client) and the server — because this unlocks one of MCP’s most powerful use cases: persistent memory. Out of the box, LLMs are goldfish. They forget everything unless you manually stuff the entire history into the context window. But with MCP, you can: Store and retrieve past interactions Keep track of long-term user preferences Build assistants that actually “remember” full projects or ongoing sessions No more clunky prompt-chaining hacks or fragile memory workarounds. MCP gives your model a brain that lasts longer than a single chat. Core Capabilities of an MCP Server With all that in mind, it’s pretty clear: the MCP server is the MVP of the whole protocol. It’s the central hub that defines the capabilities your model can actually use. There are three main types: Resources: Think of these as external data sources — PDFs, APIs, databases. The model can pull them in for context, but it can’t change them. Read-only. Tools: These are the actual functions the model can call — run code, search the web, generate summaries, you name it. Prompts: Predefined templates that guide the model’s behavior or structure its responses. Like giving it a playbook. What makes MCP powerful is that all of these are exposed through a single, consistent protocol. That means the model can request, invoke, and incorporate them without needing custom logic for each one. Just plug into the MCP server, and everything’s ready to go. Real-World Example: Claude Desktop + MCP (Pre-built Servers) Out of the box, Anthropic offers a bunch of pre-built MCP servers you can plug into your AI apps — things like Claude Desktop, Cursor, and more. Setup is super quick and painless. For the full list of available servers, head over to the MCP Servers Repository. It’s your buffet of ready-to-use integrations. In this section, I’ll walk you through a practical example: extending Claude Desktop so it can read from your computer’s file system, write new files, move them around, and even search through them. This walkthrough is based on the Quickstart guide from the official docs, but honestly, that guide skips a few key details — especially if you’ve never touched these settings before. So I’m filling in the gaps and sharing the extra tips I picked up along the way to save you the headache. 1. Download Claude Desktop First things first — grab Claude Desktop. Choose the version for macOS or Windows (sorry Linux folks, no support just yet). Follow the installation steps as prompted. Already have it installed? Make sure you’re on the latest version by clicking the Claude menu on your computer and selecting “Check for Updates…” 2. Check the Prerequisites You’ll need Node.js installed on your machine to get this running smoothly. To check if you already have Node installed: On macOS: Open the Terminal from your Applications folder. On Windows: Press Windows + R, type cmd, and hit Enter. Then run the following command in your terminal: node --version If you see a version number, you’re good to go. If not, head over to nodejs.org and install the latest LTS version. 3. Enable Developer Mode Open Claude Desktop and click on the “Claude” menu in the top-left corner of your screen. From there, select Help. On macOS, it should look something like this: Image by author From the drop-down menu, select “Enable Developer Mode.” If you’ve already enabled it before, it won’t show up again — but if this is your first time, it should be right there in the list. Once Developer Mode is turned on: Click on “Claude” in the top-left menu again. Select “Settings.” A new pop-up window will appear — look for the “Developer” tab in the left-hand navigation bar. That’s where all the good stuff lives. Image by author 4. Set Up the Configuration File Still in the Developer settings, click on “Edit Config.” This will create a configuration file if one doesn’t already exist and open it directly in your file system. The file location depends on your OS: macOS: ~/Library/Application\ Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json This is where you’ll define the servers and capabilities you want Claude to use — so keep this file open, we’ll be editing it next. Image by author Open the config file (claude_desktop_config.json) in any text editor. Replace its contents with the following, depending on your OS: For macOS: { "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop", "/Users/username/Downloads" ] } } } For Windows: { "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\username\\Desktop", "C:\\Users\\username\\Downloads" ] } } } Make sure to replace "username" with your actual system username. The paths listed here should point to valid folders on your machine—this setup gives Claude access to your Desktop and Downloads, but you can add more paths if needed. What This Does This config tells Claude Desktop to automatically start an MCP server called "filesystem" every time the app launches. That server runs using npx and spins up @modelcontextprotocol/server-filesystem, which is what lets Claude interact with your file system—read, write, move files, search directories, etc. Command Privileges Just a heads-up: Claude will run these commands with your user account’s permissions, meaning it can access and modify local files. Only add commands to the config file if you understand and trust the server you’re hooking up — no random packages from the internet! 5. Restart Claude Once you’ve updated and saved your configuration file, restart Claude Desktop to apply the changes. After it boots up, you should see a hammer icon in the bottom-left corner of the input box. That’s your signal that the developer tools — and your custom MCP server — are up and running. Image by author After clicking the hammer icon, you should see the list of tools exposed by the Filesystem MCP Server — things like reading files, writing files, searching directories, and so on. Image by author If you don’t see your server listed or nothing shows up, don’t worry. Jump over to the Troubleshooting section in the official documentation for some quick debugging tips to get things back on track. 6. Try It Out! Now that everything’s set up, you can start chatting with Claude about your file system — and it should know when to call the right tools. Here are a few things you can try asking: “Can you write a poem and save it to my Desktop?” “What are some work-related files in my Downloads folder?” “Can you take all the images on my Desktop and move them to a new folder called ‘Images’?” When needed, Claude will automatically invoke the appropriate tools and ask for your approval before doing anything on your system. You stay in control, and Claude gets the job done. Build Your Own: Custom MCP Server from Scratch Alright, ready to level up? In this section, you’ll go from user to builder. We’re going to write a custom MCP server that Claude can talk to — specifically, a tool that lets it search the latest documentation from AI libraries like LangChain, OpenAI, MCP (yes, we’re using MCP to learn MCP), and LlamaIndex. Because let’s be honest — how many times have you watched Claude confidently spit out deprecated code or reference libraries that haven’t been updated since 2021? This tool uses real-time search, scrapes live content, and gives your assistant fresh knowledge on demand. Yes, it’s as cool as it sounds. The project is built using the official MCP SDK from Anthropic. If you’re comfortable with Python and the command line, you’ll be up and running in no time. And even if you’re not — don’t worry. We’ll walk through everything step by step, including the parts most tutorials just assume you already know. Prerequisites Before we dive in, here are the things you need installed on your system: Python 3.10 or higher — this is the programming language we’ll use MCP SDK (v1.2.0 or higher) — this gives you all the tools to create a Claude-compatible server (which will be installed in upcoming parts) uv (package manager) — think of it like a modern version of pip, but much faster and easier to use for projects (which will be installed in upcoming parts) Step 1: Install uv (the Package Manager) If you’ve used Python before, you might be used to pip. uv is like pip’s cooler, more modern cousin. We’ll use it to set up and manage our project. Run the command below in your terminal to install uv: On macOS/Linux: curl –LsSf https://astral.sh/uv/install.sh | sh On Windows: powershell –ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" This will download and install uv on your machine. Once it’s done, close and reopen your terminal to make sure the uv command is recognized. (If you’re on Windows, you can use WSL or follow their Windows instructions.) To check that it’s working, run this command in your terminal: uv --version If you see a version number, you’re good to go. Step 2: Set Up Your Project Now we’re going to create a folder for our MCP server and get all the pieces in place. In your terminal, run these commands: # Create and enter your project folder uv init mcp-server cd mcp-server # Create a virtual environment uv venv # Activate the virtual environment source .venv/bin/activate # Windows: .venv\Scripts\activate Wait — what’s all this? uv init mcp-server sets up a blank Python project named mcp-server . uv venv creates a virtual environment (your private sandbox for this project). source .venv/bin/activate turns on that environment so everything you install stays inside it. Step 3: Install the Required Packages Inside your virtual environment, install the tools you’ll need: uv add "mcp[cli]" httpx beautifulsoup4 python-dotenv Here’s what each package does: mcp[cli]: The core SDK that lets you build servers Claude can talk to httpx: Used to make HTTP requests (like fetching data from websites) beautifulsoup4: Helps us extract readable text from messy HTML python-dotenv: Lets us load API keys from a .env file Before we start writing code, it’s a good idea to open the project folder in a text editor so you can see all your files in one place and edit them easily. If you’re using VS Code (which I highly recommend if you’re not sure what to use), just run this from inside your mcp-server folder: code . This command tells VS Code to open the current folder (. just means “right here”). If the code command doesn’t work, you probably need to enable it: 1. Open VS Code 2. Press Cmd+Shift+P (or Ctrl+Shift+P on Windows) 3. Type: Shell Command: Install 'code' command in PATH 4. Hit Enter, then restart your terminal If you’re using another editor like PyCharm or Sublime Text, you can just open the mcp-server folder manually from within the app. Step 3.5: Get Your Serper API Key (for Web Search) To power our real-time documentation search, we’ll use Serper — a simple and fast Google Search API that works great for AI agents. Here’s how to set it up: Head over to serper.dev and click Sign Up:It’s free for basic usage and works perfectly for this project. Once signed in, go to your Dashboard:You’ll see your API Key listed there. Copy it. In your project folder, create a file called .env:<br>This is where we’ll store the key securely (so we’re not hardcoding it). Add this line to your .env file: SERPER_API_KEY=your-api-key-here Replace your-api-key-here with the actual key you copied That’s it — now your server can talk to Google via Serper and pull in fresh docs when Claude asks. Step 4: Write the Server Code Now that your project is set up and your virtual environment is running, it’s time to actually write the server. This server is going to: Accept a question like: “How do I use retrievers in LangChain?” Know which documentation site to search (e.g., LangChain, OpenAI, etc.) Use a web search API (Serper) to find the best links from that site Visit those pages and scrape the actual content Return that content to Claude This is what makes your Claude smarter — it can look things up from real docs instead of making things up based on old data. Quick Reminder About Ethical Scraping Always respect the site you’re scraping. Use this responsibly. Avoid hitting pages too often, don’t scrape behind login walls, and check the site’s robots.txt file to see what’s allowed. You can read more about it here. Your tool is only as useful as it is respectful. That’s how we build AI systems that are not just smart — but sustainable too. 1. Create Your Server File First, run this from inside your mcp-server folder to create a new file: touch main.py Then open that file in your editor (if it isn’t open already). Replace the code there with the following: from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv import httpx import json import os from bs4 import BeautifulSoup load_dotenv() mcp = FastMCP("docs") USER_AGENT = "docs-app/1.0" SERPER_URL = "https://google.serper.dev/search" docs_urls = { "langchain": "python.langchain.com/docs", "llama-index": "docs.llamaindex.ai/en/stable", "openai": "platform.openai.com/docs", "mcp": "modelcontextprotocol.io" } async def search_web(query: str) -> dict | None: payload = json.dumps({"q": query, "num": 2}) headers = { "X-API-KEY": os.getenv("SERPER_API_KEY"), "Content-Type": "application/json", } async with httpx.AsyncClient() as client: try: response = await client.post( SERPER_URL, headers=headers, data=payload, timeout=30.0 ) response.raise_for_status() return response.json() except httpx.TimeoutException: return {"organic": []} except httpx.HTTPStatusError as e: print(f"HTTP error occurred: {e}") return {"organic": []} async def fetch_url(url: str) -> str: async with httpx.AsyncClient(headers={"User-Agent": USER_AGENT}) as client: try: response = await client.get(url, timeout=30.0) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") # Try to extract main content and remove navigation, sidebars, etc. main_content = soup.find("main") or soup.find("article") or soup.find("div", class_="content") if main_content: text = main_content.get_text(separator="\n", strip=True) else: text = soup.get_text(separator="\n", strip=True) # Limit content length if it's too large if len(text) > 8000: text = text[:8000] + "... [content truncated]" return text except httpx.TimeoutException: return "Timeout error when fetching the URL" except httpx.HTTPStatusError as e: return f"HTTP error occurred: {e}" @mcp.tool() async def get_docs(query: str, library: str) -> str: """ Search the latest docs for a given query and library. Supports langchain, openai, mcp and llama-index. Args: query: The query to search for (e.g. "Chroma DB") library: The library to search in (e.g. "langchain") Returns: Text from the docs """ if library not in docs_urls: raise ValueError(f"Library {library} not supported by this tool. Supported libraries: {', '.join(docs_urls.keys())}") query = f"site:{docs_urls[library]} {query}" results = await search_web(query) if not results or len(results.get("organic", [])) == 0: return "No results found" combined_text = "" for i, result in enumerate(results["organic"]): url = result["link"] title = result.get("title", "No title") # Add separator between results if i > 0: combined_text += "\n\n" + "="*50 + "\n\n" combined_text += f"Source: {title}\nURL: {url}\n\n" page_content = await fetch_url(url) combined_text += page_content return combined_text if __name__ == "__main__": mcp.run(transport="stdio") 2. How The Code Works First, we set up the foundation of our custom MCP server. It pulls in all the libraries you’ll need — like tools for making web requests, cleaning up webpages, and loading secret API keys. It also creates your server and names it "docs" so Claude knows what to call. Then, it lists the documentation sites (like LangChain, OpenAI, MCP, and LlamaIndex) your tool will search through. Finally, it sets the URL for the Serper API, which is what the tool will use to send Google search queries. Think of it as prepping your workspace before actually building the tool. Click here to see the revelant code snippet from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv import httpx import json import os from bs4 import BeautifulSoup load_dotenv() mcp = FastMCP("docs") USER_AGENT = "docs-app/1.0" SERPER_URL = "https://google.serper.dev/search" docs_urls = { "langchain": "python.langchain.com/docs", "llama-index": "docs.llamaindex.ai/en/stable", "openai": "platform.openai.com/docs", "mcp": "modelcontextprotocol.io" } Then, we define a function that lets our tool talk to the Serper API, which we’ll use as a wrapper around Google Search. This function, search_web, takes in a query string, builds a request, and sends it off to the search engine. It includes your API key for authentication, tells Serper we’re sending JSON, and limits the number of search results to 2 for speed and focus. The function returns a dictionary containing the structured results, and it also gracefully handles timeouts or any errors that might come from the API. This is the part that helps Claude figure out where to look before we even fetch the content. Click here to see the relevant code snippet async def search_web(query: str) -> dict | None: payload = json.dumps({"q": query, "num": 2}) headers = { "X-API-KEY": os.getenv("SERPER_API_KEY"), "Content-Type": "application/json", } async with httpx.AsyncClient() as client: try: response = await client.post( SERPER_URL, headers=headers, data=payload, timeout=30.0 ) response.raise_for_status() return response.json() except httpx.TimeoutException: return {"organic": []} except httpx.HTTPStatusError as e: print(f"HTTP error occurred: {e}") return {"organic": []} Once we’ve found a few promising links, we need a way to extract just the useful content from those web pages. That’s what fetch_url does. It visits each URL, grabs the full HTML of the page, and then uses BeautifulSoup to filter out just the readable parts—things like paragraphs, headings, and examples. We try to prioritize sections like <main>, <article>, or containers with a .content class, which usually hold the good stuff. If the page is super long, we also trim it down to avoid flooding the output. Think of this as the “reader mode” for Claude—it turns cluttered webpages into clean text it can understand. Click here to see the relevant code snippet async def fetch_url(url: str) -> str: async with httpx.AsyncClient(headers={"User-Agent": USER_AGENT}) as client: try: response = await client.get(url, timeout=30.0) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") # Try to extract main content and remove navigation, sidebars, etc. main_content = soup.find("main") or soup.find("article") or soup.find("div", class_="content") if main_content: text = main_content.get_text(separator="\n", strip=True) else: text = soup.get_text(separator="\n", strip=True) # Limit content length if it's too large if len(text) > 8000: text = text[:8000] + "... [content truncated]" return text except httpx.TimeoutException: return "Timeout error when fetching the URL" except httpx.HTTPStatusError as e: return f"HTTP error occurred: {e}" Now comes the main act: the actual tool function that Claude will call. The get_docs function is where everything comes together. Claude will pass it a query and the name of a library (like "llama-index"), and this function will: Check if that library is supported Build a site-specific search query (e.g., site:docs.llamaindex.ai "vector store") Use search_web() to get the top results Use fetch_url() to visit and extract the content Format everything into a nice, readable string that Claude can understand and return We also include titles, URLs, and some visual separators between each result, so Claude can reference or cite them if needed. Click here to see the relevant code snippet @mcp.tool() async def get_docs(query: str, library: str) -> str: """ Search the latest docs for a given query and library. Supports langchain, openai, mcp and llama-index. Args: query: The query to search for (e.g. "Chroma DB") library: The library to search in (e.g. "langchain") Returns: Text from the docs """ if library not in docs_urls: raise ValueError(f"Library {library} not supported by this tool. Supported libraries: {', '.join(docs_urls.keys())}") query = f"site:{docs_urls[library]} {query}" results = await search_web(query) if not results or len(results.get("organic", [])) == 0: return "No results found" combined_text = "" for i, result in enumerate(results["organic"]): url = result["link"] title = result.get("title", "No title") # Add separator between results if i > 0: combined_text += "\n\n" + "="*50 + "\n\n" combined_text += f"Source: {title}\nURL: {url}\n\n" page_content = await fetch_url(url) combined_text += page_content return combined_text Finally, this line kicks everything off. It tells the MCP server to start listening for input from Claude using standard input/output (which is how Claude Desktop talks to external tools). This line always lives at the bottom of your script. if __name__ == "__main__": mcp.run(transport="stdio") Step 5: Test and Run Your Server Alright, your server is coded and ready to go — now let’s run it and see it in action. There are two main ways to test your MCP server: Development Mode (Recommended for Building & Testing) The easiest way to test your server during development is to use: mcp dev main.py This command launches the MCP Inspector, which opens up a local web interface in your browser. It’s like a control panel for your server. Image by author Here’s what you can do with it: Interactively test your tools (like get_docs) View detailed logs and error messages in real time Monitor performance and response times Set or override environment variables temporarily Use this mode while building and debugging. You’ll be able to see exactly what Claude would see and quickly fix any issues before integrating with the full Claude Desktop app. Claude Desktop Integration (For Regular Use) Once your server works and you’re happy with it, you can install it into Claude Desktop: mcp install main.py This command will: Add your server into Claude’s configuration file (the JSON file we fiddled with earlier) automatically Enable it to run every time you launch Claude Desktop Make it available through the Developer Tools ( hammer icon) But hold on — there’s one small catch… Current Issue: uv Command Is Hardcoded Right now, there’s an open issue in the mcp library: when it writes your server into Claude’s config file, it hardcodes the command as just "uv". That works only if uv is globally available in your PATH — which isn’t always the case, especially if you installed it locally with pipx or a custom method. So we need to fix it manually. Here’s how: Manually Update Claude’s Config File Open your Claude config file: On MacOS: code ~/Library/Application\ Support/Claude/claude_desktop_config.json On Windows: code $env:AppData\Claude\claude_desktop_config.json If you’re not using VS Code, replace code with your text editor of choice (like open, nano, or subl). 2. Find the section that looks like this: "docs": { "command": "uv", "args": [ "run", "--with", "mcp[cli]", "mcp", "run", "/PATH/TO/mcp-server/main.py" ] } 3. Update the "command" value to the absolute path of uv on your system. To find it, run this in your terminal: which uv It’ll return something like: /Users/your_username/.local/bin/uv Now replace "uv" in the config with that full path: "docs": { "command": "/Users/your_username/.local/bin/uv", "args": [ "run", "--with", "mcp[cli]", "mcp", "run", "PATH/TO/mcp-server/main.py" ] } 4. Save the file and restart Claude Desktop. That’s It! Now Claude Desktop will recognize your custom docs tool, and anytime you open the Developer Tools (), it’ll show up. You can chat with Claude and ask things like: “Can you check the latest MCP docs for how to build a custom server?” And Claude will call your server, search the docs, pull the content, and use it in its response — live. You can view a quick demo here. Image by author Congrats, You’ve Mastered MCP! You did it. You’ve gone from zero to building, testing, and integrating your very own Claude-compatible MCP server — and that is no small feat. Take a moment. Stretch. Sip some coffee. Pat yourself on the back. You didn’t just write some Python — you built a real, production-grade tool that extends an LLM’s capabilities in a modular, secure, and powerful way. Seriously, most devs don’t get this far. You now understand: How MCP works under the hood How to build and expose tools Claude can use How to wire up real-time web search and content extraction How to debug, test, and integrate the whole thing with Claude Desktop You didn’t just learn it — you shipped it. Want to go even deeper? There’s a whole world of agentic workflows, custom tools, and collaborative LLMs waiting to be built. But for now? Take the win. You earned it. Now go ask Claude something fun and let your new tool flex. References [1] Anthropic, Model Context Protocol: Introduction (2024), modelcontextprotocol.io[2] LangChain, MCP From Scratch (2024), Notion[3] A. Alejandro, MCP Server Example (2024), GitHub Repository[4] O. Santos, Integrating Agentic RAG with MCP Servers: Technical Implementation Guide (2024), Medium The post How I Finally Understood MCP — and Got It Working in Real Life appeared first on Towards Data Science.
·29 Visualizações