MCP for Developers: A No-Nonsense Guide

MCP for Developers: A No-Nonsense Guide

Tags
AI/ML
Published
May 14, 2025
Author
Akshay Kripalani
This article assumes you have read the previous one (link), and have a general understanding of what MCP is, and why it’s useful. Let's dive a little deeper into its internal workings.
Primarily, MCP follows a client-server architecture. The best way to think about it is like accessing any generic FastAPI or Express.js webserver. There are 3 parts to this whole system:
  1. MCP Server: This is the heart. This is the server that runs the logic of your whole MCP. It defines interaction patterns with your tools (e.g. Specific API Calls, Processing/Modifying documents, etc.). It can interact with local files and data, as well as with external systems like APIs.
  1. MCP Client: This is what interacts with your webserver. Usually, this is not something you have to worry about - this handles the interaction with the webserver and sending data back to you.
  1. MCP Host: This is how you interact with it. Apps like Claude Desktop, IDEs like Cursor, or AI tools that want to access data and functions through MCP - these initiate connections.
 
To visualise:
 
Now that we understand how an MCP works, let’s get our hands dirty. The goal of this blog is to build a demo MCP that allows us to automatically find good first issues to fix. The "good first issue" tag on GitHub highlights beginner-friendly issues in a repository, making them ideal for new contributors to get started.
But before we get started, let’s first ensure we have successfully run a pre-built mcp server locally. If you have already run server locally and want to get to building, you can skip this section.
  1. Ensure you have the latest NodeJS version installed (v22.14.0)
  1. Download Claude for Desktop - this will be our primary method of interaction for MCP Servers.
    1. Note: Claude for Desktop is not yet supported on Linux. Linux users can use IDEs like Cursor that support MCP integration.
  1. To get started with MCP, we will be installing the Filesystem MCP Server to run locally. This allows Claude to directly interact with any files you may have installed locally (It will ask you before making any modifications, so its completely safe!)
  1. Get started by going to the settings menu (should look like this on windows)
notion image
  1. Go to Developer settings, and click Edit Config. This will take you to claude_desktop_config.json, where the MCP magic happens. Paste the following code in.
    1. For windows:
{ "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\username\\Desktop", "C:\\Users\\username\\Downloads" ] } } }
  1. For mac:
{ "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop", "/Users/username/Downloads" ] } } }
  1. Restart your claude app, and your MCP Should work!. If it connected successfully, it will look like this, with MCP tools integrated.
notion image
  1. Done! If it didn’t work, refer here for troubleshooting.
Now that that’s sorted out, lets get to building our own MCP Server.
  1. MCP Official documentation recommends we use uv as the preferred package manager, hence we go through the following steps to initialise our server:
    1. pip install uv
    2. uv init .
    3. uv venv
    4. uv add requests python-dotenv fastapi “mcp[cli]”
  1. Since our goal is to find good first issues, we can use GitHub’s official API. Get your GitHub personal access token, before we move to the next step.
  1. Import the correct libraries
Note!: The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools. Thus, use docstrings and type hints generously.
import os import requests from mcp.server.fastmcp import FastMCP from fastapi import HTTPException from pydantic import BaseModel from dotenv import load_dotenv # Initialize FastMCP mcp = FastMCP("github-issues")
  1. Define Pydantic basemodels, as these help the LLM Understand the specific queries the tool accepts.
class IssueRequest(BaseModel): """ Request model for GitHub issue search. Attributes: topic: The topic or technology to search for issues in. """ topic: str # Response Structure class IssueInfo(BaseModel): """ Response model containing information about a GitHub issue. Attributes: title: The title of the issue. url: The HTML URL to view the issue on GitHub. repository: The repository name in 'owner/repo' format. body: The full description/body of the issue. """ title: str url: str repository: str body: str
  1. Define Helper functions. In this case we extract the owner name and repo name.
def get_repo_name_from_url(repo_url: str) -> str: """Extracts 'owner/repo' from a GitHub repository API URL.""" parts = repo_url.split('/') if len(parts) >= 2: return f"{parts[-2]}/{parts[-1]}" return repo_url
  1. Define the tool execution and its logic. In this case, we use our github token to query the API, and find “good first issues”. These are ideal for people contributing newly to opensource. The Docstring is important, as it gives context to the LLM on when to call it
async def find_issues(request: IssueRequest) -> List[IssueInfo]: """Searches GitHub for open issues labeled 'good first issue' related to the provided topic. This tool is specifically designed to help newcomers to open source find beginner-friendly issues to work on. It identifies issues that project maintainers have explicitly marked as suitable for first-time contributors, making it easier for newbies to find good first problems to solve and start their open source journey.""" if not os.getenv("GITHUB_TOKEN"): raise HTTPException(status_code=500, detail="Server configuration error: GitHub token missing.") topic = request.topic.strip() if not topic: raise HTTPException(status_code=400, detail="Topic cannot be empty.") query = f'{topic} is:issue state:open label:"good first issue"' # Setup Headers headers = { "Accept": "application/vnd.github.v3+json", "Authorization": f"Bearer {os.getenv('GITHUB_TOKEN')}", "X-GitHub-Api-Version": "2022-11-28", } # Query Parameters params = { "q": query, "sort": "updated", "order": "desc", "per_page": 6 } # Hit API Endpoint try: response = requests.get("https://api.github.com/search/issues", headers=headers, params=params, timeout=10) response.raise_for_status() except requests.exceptions.RequestException as e: raise HTTPException(status_code=503, detail=f"Error contacting GitHub API: {e}") data = response.json() issues_found: list[IssueInfo] = [] # Formulate response if "items" in data: for item in data["items"][:6]: if item.get("title") and item.get("html_url") and item.get("repository_url"): issue = IssueInfo( title=item["title"], url=item["html_url"], repository=get_repo_name_from_url(item["repository_url"]), body=item.get("body", "") ) issues_found.append(issue) return issues_found
  1. Define main, and run server! Run uv run main.py to ensure everything is working.
if __name__ == "__main__": load_dotenv() mcp.run(transport="stdio")
To access this server, add it to what your client expects to know about MCP servers. In the case of Claude Desktop, add the following under “mcpServers” in your claude_desktop_config.json
"github-issues": { "command": "uv", "args": [ "--directory", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/github-issues", "run", "main.py" ] }
Restart the claude app, and you’ll see the tool!
notion image
And watch the magic happen!:
notion image
To summarise, this is what happens under the hood when you send a query:
  • The client sends your question to Claude
  • Claude analyzes the available tools and decides which one(s) to use
  • The client executes the chosen tool(s) through the MCP server
  • The results are sent back to Claude
  • Claude formulates a natural language response
  • The response is displayed to you!
Limitations:
  • MCP servers are stateful by nature, meaning they maintain constant connections and are comparatively more expensive to stateless servers.
  • MCP is currently in active development. Meaning features like Stateless, SSE, are works in progress.
MCP proves to be incredibly useful to build apps that programmatically interact with other apps. Every API can be an MCP! What are your thoughts on MCP? We’d love to hear them.