Disclaimer: This post has been translated to English using a machine translation model. Please, let me know if you find any mistakes.
📚 **This entry is part of the _MCP with FastMCP_ series**, divided into four chapters that are read in order:
> * 👉 **Part 1: First server and tools**
* Part 2: Transport, context and resources
* Part 3: Advanced resources and prompts
* Part 4: HTTP, authentication and client
What is MCP?
MCP (Model Context Protocol) is an open source standard developed by Anthropic to allow AI models to interact with external tools through a standard
Until the development of the MCP protocol, when we wanted an LLM to interact with tools, we had to create code to be able to interact with the tool, and using function calling send the information to the LLM.
So, through MCP, an LLM can interact with tools thanks to a standard. In this way, if someone creates an MCP server, that server can be reused by others with a single client. If you develop a client in your application, you can download an MCP server developed by someone else and use it without any problem.
Commonly, MCP is similar to the USB standard. Before USB, each peripheral had a different type of connection; some had serial ports, others parallel. Different connector formats, etc.
With the arrival of USB, all peripherals adapted to this standard, so with a single USB connector on your computer, you can connect almost any peripheral.
MCP has 7 main components:
- Host: LLM application that has access to MCP tools.
- MCP Server: Server that performs communication with the API or tool we want to expose to the LLM
- MCP Client: Client that connects to the MCP server and makes requests
- Tool: Function that runs on the MCP server and can be invoked by the LLM
- Resource: Resource that can be used on the MCP server. They usually give the LLM access to static resources such as files, databases, etc.
- Resource template: Template for creating dynamic resources. Using these templates, the LLM can dynamically create the resource it wants to access
- Prompt: Prompt used to generate a prompt that will be used by the LLM to interact with the MCP server.
A single host (application) can have multiple clients. Each client will connect to an MCP server.
FastMCP
Although the MCP documentation recommends installing mcp["cli"], there is a library built on top of it called fastmcp, which is very helpful when creating MCP servers, so we are going to use it.
Create virtual environment
To create an MCP server and client, we’re going to create virtual environments with uv and the dependencies we’ll need
MCP Server
First, we create a folder for the MCP server.
InputPython!mkdir gitHub_MCP_serverCopied
We start the uv environment
InputPython!cd gitHub_MCP_server && uv init .Copied
Initialized project `github-mcp-server` at `/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server`
We activated it
InputPython!cd gitHub_MCP_server && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
And we install the necessary libraries
InputPython!cd gitHub_MCP_server && uv add anthropic fastmcp python-dotenv requestsCopied
Resolved 42 packages in 34msInstalled 40 packages in 71ms+ annotated-types==0.7.0+ anyio==4.9.0+ authlib==1.6.0+ certifi==2025.6.15+ cffi==1.17.1+ charset-normalizer==3.4.2+ click==8.2.1+ cryptography==45.0.4+ distro==1.9.0+ exceptiongroup==1.3.0+ fastmcp==2.9.0+ h11==0.16.0+ httpcore==1.0.9+ httpx==0.28.1+ httpx-sse==0.4.0+ idna==3.10+ jiter==0.10.0+ markdown-it-py==3.0.0+ mcp==1.9.4+ mdurl==0.1.2+ openapi-pydantic==0.5.1+ pycparser==2.22+ pydantic==2.11.7+ pydantic-core==2.33.2+ pydantic-settings==2.10.0+ pygments==2.19.2+ python-dotenv==1.1.1+ python-multipart==0.0.20+ requests==2.32.4+ rich==14.0.0+ shellingham==1.5.4+ sniffio==1.3.1+ sse-starlette==2.3.6+ starlette==0.47.1+ typer==0.16.0+ typing-extensions==4.14.0+ typing-inspection==0.4.1+ typing-inspection==0.4.1
MCP Client
Now we create a folder where we will program the MCP client
InputPython!mkdir client_MCPCopied
We start the uv environment
InputPython!cd client_MCP && uv init .Copied
Initialized project `client-mcp` at `/Users/macm1/Documents/web/portafolio/posts/client_MCP`
We activated it
InputPython!cd client_MCP && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
And finally, we install the necessary libraries for the client.
InputPython!cd client_MCP && uv add anthropic fastmcp python-dotenv requestsCopied
Resolved 42 packages in 307msPrepared 5 packages in 115msInstalled 40 packages in 117ms+ annotated-types==0.7.0+ anthropic==0.55.0+ anyio==4.9.0+ authlib==1.6.0+ certifi==2025.6.15+ cffi==1.17.1+ charset-normalizer==3.4.2+ click==8.2.1+ cryptography==45.0.4+ distro==1.9.0+ exceptiongroup==1.3.0+ fastmcp==2.9.0+ h11==0.16.0+ httpcore==1.0.9+ httpx==0.28.1+ httpx-sse==0.4.0+ idna==3.10...+ requests==2.32.4+ rich==14.0.0+ shellingham==1.5.4+ sniffio==1.3.1+ sse-starlette==2.3.6+ starlette==0.47.1+ typer==0.16.0+ typing-extensions==4.14.0+ typing-inspection==0.4.1+ typing-inspection==0.4.1
We are going to use Sonnet 3.5 as the LLM model, so we create a .env file in the client folder with the Claude API Key that can be obtained from the keys page of the Claude API
InputPython%%writefile client_MCP/.envANTHROPIC_API_KEY="ANTHROPIC_API_KEY"Copied
Writing client_MCP/.env
Basic MCP
We write the minimum code we need to have an MCP server
InputPython%%writefile gitHub_MCP_server/github_server.pyfrom mcp.server.fastmcp import FastMCP# Create an MCP servermcp = FastMCP("GitHubMCP")if __name__ == "__main__":# Initialize and run the servermcp.run(transport='stdio')Copied
Overwriting gitHub_MCP_server/github_server.py
As you can see, we need to create a FastMCP object and then run the server with mcp.run.
Library with functions for reading from GitHub
Since we are going to create an MCP server to use GitHub utilities, we are going to create a file with the necessary functions to build the headers required to use the GitHub API.
InputPython%%writefile gitHub_MCP_server/github.pyimport osfrom dotenv import load_dotenv# Load the GitHub token from the .env fileload_dotenv()GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")# Check if the GitHub token is configuredif not GITHUB_TOKEN:print("WARNING: The GITHUB_TOKEN environment variable is not configured.")print("Requests to the GitHub API may fail due to rate limits.")print("Create a .env file in this directory with GITHUB_TOKEN='your_token_here'")raise ValueError("GITHUB_TOKEN is not configured")# Helper function to create headers for GitHub API requestsdef create_github_headers():headers = {}if GITHUB_TOKEN:headers["Authorization"] = f"Bearer {GITHUB_TOKEN}"# GitHub recommends including a User-Agentheaders["User-Agent"] = "MCP_GitHub_Server_Example"headers["Accept"] = "application/vnd.github.v3+json" # Good practicereturn headersCopied
Overwriting gitHub_MCP_server/github.py
To be able to build the headers, we need a GitHub token. To do this, go to personal-access-tokens and create a new token. We copy it
Now, we create a .env file, where we are going to store the GitHub token.
InputPython%%writefile gitHub_MCP_server/.envGITHUB_TOKEN = "GITHUB_TOKEN"Copied
Overwriting gitHub_MCP_server/.env
Create an MCP tool to get a list of issues from a GitHub repository
MCP Server
We added a function to be able to list the issues of a GitHub repository. To turn that function into an MCP tool, we use the @mcp.tool() decorator
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create a FastMCP servermcp = FastMCP("GitHubMCP")@mcp.tool()async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to the first 10 issues to avoid long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'Sin título')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comentarios)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Mostrando los primeros 10 issues abiertos" if len(issues_summary) == 10 else f"Mostrando todos los {len(issues_summary)} issues abiertos","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting GitHub FastMCP server...")print(f"DEBUG: Server name: {mcp.name}")print("DEBUG: Available tools: list_repository_issues")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
MCP Client
Now we create an MCP client so we can use the tool we have created
InputPython%%writefile client_MCP/client.pyimport sysimport asynciofrom contextlib import AsyncExitStackfrom anthropic import Anthropicfrom dotenv import load_dotenvfrom fastmcp import Client# Load environment variables from .env fileload_dotenv()class FastMCPClient:"""FastMCP client that integrates with Claude to process user queriesand use tools exposed by a FastMCP server."""def __init__(self):"""Initialize the FastMCP client with Anthropic and resource management."""self.exit_stack = AsyncExitStack()self.anthropic = Anthropic()self.client = Noneasync def connect_to_server(self, server_script_path: str):"""Connect to the specified FastMCP server.Args:server_script_path: Path to the server script (Python)"""print(f"🔗 Connecting to FastMCP server: {server_script_path}")# Determine the server type based on the extensionif not server_script_path.endswith('.py'):raise ValueError(f"Unsupported server type. Use .py files. Received: {server_script_path}")# Create FastMCP clientself.client = Client(server_script_path)# Note: FastMCP Client automatically infers transport from .py filesprint("✅ Client created successfully")async def list_available_tools(self):"""List available tools in the FastMCP server."""try:# Get list of tools from the server using FastMCP contextasync with self.client as client:tools = await client.list_tools()if tools:print(f" 🛠️ Available tools ({len(tools)}):")print("=" * 50)for tool in tools:print(f"📋 {tool.name}")if tool.description:print(f" Description: {tool.description}")# Show parameters if availableif hasattr(tool, 'inputSchema') and tool.inputSchema:if 'properties' in tool.inputSchema:params = list(tool.inputSchema['properties'].keys())print(f" Parameters: {', '.join(params)}")print()else:print("⚠️ No tools found in the server")except Exception as e:print(f"❌ Error listing tools: {str(e)}")async def process_query(self, query: str) -> str:"""Process a user query, interacting with Claude and FastMCP tools.Args:query: User queryReturns:str: Final processed response"""try:# Use FastMCP context for all operationsasync with self.client as client:# Get available toolstools_list = await client.list_tools()# Prepare tools for Claude in correct formatclaude_tools = []for tool in tools_list:claude_tool = {"name": tool.name,"description": tool.description or f"Tool {tool.name}","input_schema": tool.inputSchema or {"type": "object", "properties": {}}}claude_tools.append(claude_tool)# Create initial message for Claudemessages = [{"role": "user","content": query}]# First call to Clauderesponse = self.anthropic.messages.create(model="claude-3-5-sonnet-20241022",max_tokens=6000,messages=messages,tools=claude_tools if claude_tools else None)# Process Claude's responseresponse_text = ""for content_block in response.content:if content_block.type == "text":response_text += content_block.textelif content_block.type == "tool_use":# Claude wants to use a tooltool_name = content_block.nametool_args = content_block.inputtool_call_id = content_block.idprint(f"🔧 Claude wants to use: {tool_name}")print(f"📝 Arguments: {tool_args}")try:# Execute tool on the FastMCP servertool_result = await client.call_tool(tool_name, tool_args)print(f"✅ Tool executed successfully")# Add tool result to the conversationmessages.append({"role": "assistant","content": response.content})# Format result for Claudeif tool_result:# Convert result to string format for Clauderesult_content = str(tool_result)messages.append({"role": "user","content": [{"type": "tool_result","tool_use_id": tool_call_id,"content": f"Tool result: {result_content}"}]})else:messages.append({"role": "user","content": [{"type": "tool_result","tool_use_id": tool_call_id,"content": "Tool executed without response content"}]})# Second call to Claude with the tool resultfinal_response = self.anthropic.messages.create(model="claude-3-5-sonnet-20241022",max_tokens=6000,messages=messages,tools=claude_tools if claude_tools else None)# Extract text from the final responsefor final_content in final_response.content:if final_content.type == "text":response_text += final_content.textexcept Exception as e:error_msg = f"❌ Error executing {tool_name}: {str(e)}"print(error_msg)response_text += f" {error_msg}"return response_textexcept Exception as e:error_msg = f"❌ Error processing query: {str(e)}"print(error_msg)return error_msgasync def chat_loop(self):"""Main chat loop with user interaction."""print(" 🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.")print("💬 You can ask questions about GitHub repositories!")print("📚 The client can use tools from the FastMCP server")print("-" * 60)while True:try:# Request user inputuser_input = input(" 👤 You: ").strip()if user_input.lower() in ['quit', 'q', 'exit', 'salir']:print("👋 Bye!")breakif not user_input:continueprint(" 🤔 Claude is thinking...")# Process queryresponse = await self.process_query(user_input)# Show responseprint(f" 🤖 Claude: {response}")except KeyboardInterrupt:print(" 👋 Disconnecting...")breakexcept Exception as e:print(f" ❌ Error in chat: {str(e)}")continueasync def cleanup(self):"""Clean up resources and close connections."""print("🧹 Cleaning up resources...")# FastMCP Client cleanup is handled automatically by context managerawait self.exit_stack.aclose()print("✅ Resources released")async def main():"""Main function that initializes and runs the FastMCP client."""# Verify command line argumentsif len(sys.argv) != 2:print("❌ Usage: python client.py <path_to_fastmcp_server>")print("📝 Example: python client.py ../MCP_github/github_server.py")sys.exit(1)server_script_path = sys.argv[1]# Create and run clientclient = FastMCPClient()try:# Connect to the serverawait client.connect_to_server(server_script_path)# List available tools after connectionawait client.list_available_tools()# Start chat loopawait client.chat_loop()except Exception as e:print(f"❌ Fatal error: {str(e)}")finally:# Ensure resources are cleaned upawait client.cleanup()if __name__ == "__main__":# Entry point of the scriptasyncio.run(main())Copied
Overwriting client_MCP/client.py
MCP Client Explanation
- In
main, it is checked that an argument has been passed with the MCP server path. - An object of the class
FastMCPClientis created with the MCP server path. When creating the object, the__init__method is executed, which creates the connection with Anthropic's LLM, which will be the LLM that will provide the "brain" - An attempt is made to connect to the MCP server by calling the
connect_to_servermethod to open a session with the MCP server. - The available
tools are listed with thelist_available_toolsmethod - If a connection could be established, the
chat_loopmethod is called, which is an infinite loop to chat with the LLM that has just been created in the client. Execution only stops whenquit,q,exit, orsaliris entered in the chat. - The user input is processed with the
process_querymethod, which obtains the list of availabletools and sends a request to the LLM with the user message and the list oftools - If the LLM responds with text, the text is returned, which will be printed
- If the LLM responds with
tool_use, the name of thetool, the arguments, and an execution ID are obtained. The tool is executed. With the tool's result, a new message is created and sent to the LLM so it can process it and generate a response, which will be returned and printed. - When the conversation ends, the
cleanupmethod will be called, which will close whatever needs to be closed.
Test of the tool
We go to the client path and run it, giving it the MCP server path.
InputPython!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.pyCopied
🔗 Connecting to FastMCP server: ../gitHub_MCP_server/github_server.py✅ Client created successfully[06/28/25 09:22:09] INFO Starting MCP server 'GitHubMCP' with transport 'stdio' server.py:1246🛠️ Available tools (1):==================================================📋 list_repository_issuesDescription: Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issueParameters: owner, repo_name🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.💬 You can ask questions about GitHub repositories!📚 The client can use tools from the FastMCP server------------------------------------------------------------👤 You: Tell me de issues of repository transformers of huggingface🤔 Claude is thinking...🔧 Claude wants to use: list_repository_issues📝 Arguments: {'owner': 'huggingface', 'repo_name': 'transformers'}✅ Tool executed successfully🤖 Claude: I'll help you list the issues from the Hugging Face transformers repository. Let me use the `list_repository_issues` function with the appropriate parameters.I'll summarize the current open issues from the Hugging Face transformers repository. Here are the 10 most recent open issues:1. [#39097] Core issue about saving models with multiple shared tensor groups when dispatched2. [#39096] Pull request to fix position index in v4.52.43. [#39095] Issue with Qwen2_5_VLVisionAttention flash attention missing 'is_causal' attribute4. [#39094] Documentation improvement for PyTorch examples5. [#39093] Style change PR for lru_cache decorator6. [#39091] Compatibility issue with sentencepiece on Windows in Python 3.137. [#39090] Pull request for fixing bugs in finetune and batch inference8. [#39089] Bug report for LlavaOnevisonConfig initialization in version 4.52.49. [#39087] Documentation PR for Gemma 3n audio encoder10. [#39084] Pull request for refactoring gemma3nNote that this is showing the 10 most recent open issues, and there might be more issues in the repository. Each issue has a link where you can find more details about the specific problem or proposed changes.Would you like more specific information about any of these issues?👤 You: q👋 Bye!🧹 Cleaning up resources...✅ Resources released
When running it, we see
🛠️ Available tools (1):
==================================================
📋 list_repository_issues
Description: Lists open issues for a given GitHub repository.
Args:
owner: The owner of the repository (e.g., 'modelcontextprotocol')
repo_name: The name of the repository (e.g., 'python-sdk')
Returns:
list[dict]: Una lista de diccionarios, cada uno de los cuales contiene información sobre un problema
Parameters: owner, repo_nameWhat indicates that the MCP client can see the tool that we have created on the MCP server.
Then we can see
👤 You: Tell me the issues of the Hugging Face transformers repository
🤔 Claude is thinking...
🔧 Calling tool: list_repository_issues
📝 Arguments: {'owner': 'huggingface', 'repo_name': 'transformers'}
✅ Tool executed successfullyWe asked for the issues of the transformers repository from huggingface. After thinking for a while, it tells us that it is going to use the tool list_repository_issues with the arguments {'owner': 'huggingface', 'repo_name': 'transformers'}.
Finally, it tells us that the tool has been executed successfully.
Finally, with the result of running the tool, Claude processes it and creates a response for us with the list of issues.
🤖 Claude: Te ayudaré a enumerar los problemas del repositorio de Hugging Face transformers. Déjame usar la función `list_repository_issues` con los parámetros apropiados. Resumiré los problemas abiertos actuales del repositorio de Hugging Face transformers. Aquí están los 10 problemas abiertos más recientes:
1. [#39097] Core issue about saving models with multiple shared tensor groups when dispatched
2. [#39096] Pull request to fix the position index in v4.52.4
3. [#39095] Issue with Qwen2_5_VLVisionAttention flash attention missing 'is_causal' attribute
4. [#39094] Documentation improvement for PyTorch examples
5. [#39093] Solicitud de cambio de estilo para el decorador lru_cache
6. [#39091] Problema de compatibilidad con sentencepiece en Windows en Python 3.13
7. [#39090] Pull request for fixing bugs in finetune and batch inference8. [#39089] Informe de error para la inicialización de LlavaOnevisonConfig en la versión 4.52.4
9. [#39087] Documentation PR for Gemma 3n audio encoder
10. [#39084] Pull request for refactoring gemma3n
Note que se muestran los 10 problemas abiertos más recientes, y puede haber más problemas en el repositorio. Cada problema tiene un enlace donde puedes encontrar más detalles sobre el problema específico o los cambios propuestos.
Would you like more specific information about any of these issues?Create the MCP server with more information
MCP Server
Earlier, we created the server with mcp = FastMCP(), but we can take advantage of this to give the server a name and description with
mcp = FastMCP(
name="GitHubMCP",
instructions="""
This server provides tools, resources and prompts to interact with the GitHub API.
"""
)InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="""This server provides tools, resources and prompts to interact with the GitHub API.""")@mcp.tool()async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')ctx: The MCP context for logging.Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
Filter tools by tags
MCP Server
MCP gives us the option to expose tools through tags, which can be useful for exposing only tools for debugging, so that only certain users can use them, etc.
To do this, when we create the MCP server, we specify the tags we want to include
mcp = FastMCP(
name="GitHubMCP",
instructions="This server provides tools, resources, and prompts to interact with the GitHub API.",
include_tags={"public"}
)And then, when we create the tool, we can specify the tags we want it to have.
@mcp.tool(tags=production)Let's see an example
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="This server provides tools, resources and prompts to interact with the GitHub API.",include_tags={"public"})@mcp.tool(tags={"public", "production"})async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"private", "development"})async def list_more_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 100 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=100"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
We can see that we have created the list_repository_issues function, which lists only 10 issues and has the tags public and production. And we have created the list_more_repository_issues function, which lists 100 issues from a repository and has the tags private and development.
Additionally, we have declared the server through
mcp = FastMCP(
name="GitHubMCP",
instructions="This server provides tools, resources and prompts to interact with the GitHub API.",
include_tags={"public"})So the client will only have access to the tools that have the public tag, that is, to list_repository_issues. They will only be able to see a list of 10 issues.
Tag testing
We run the MCP client again
InputPython!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.pyCopied
🔗 Connecting to FastMCP server: ../gitHub_MCP_server/github_server.py✅ Client created successfully[06/28/25 09:44:55] INFO Starting MCP server 'GitHubMCP' with ]8;id=896921;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py\server.py]8;;\:]8;id=507812;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py#1246\1246]8;;\transport 'stdio'🛠️ Available tools (1):==================================================📋 list_repository_issuesDescription: Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')ctx: The MCP context for logging.Returns:list[dict]: A list of dictionaries, each containing information about an issueParameters: owner, repo_name🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.💬 You can ask questions about GitHub repositories!📚 The client can use tools from the FastMCP server------------------------------------------------------------👤 You:
There's no need to make a request, since we see the following:
🛠️ Available tools (1):
==================================================
📋 list_repository_issues
Description: Lists open issues for a given GitHub repository.
Args:
owner: The owner of the repository (e.g., 'modelcontextprotocol')
repo_name: The name of the repository (e.g., 'python-sdk')
ctx: El contexto MCP para el registro.
Returns:
list[dict]: Una lista de diccionarios, cada uno contiene información sobre un issue
Parameters: owner, repo_nameThat is, the client can only see the tool list_repository_issues and not the tool list_all_repository_issues.
Change to private
We changed include_tags to private to use the tool list_more_repository_issues
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="This server provides tools, resources and prompts to interact with the GitHub API.",include_tags={"private"})@mcp.tool(tags={"public", "production"})async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"private", "development"})async def list_more_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 100 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=100"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
Private tag test
We run the client again with the change made
InputPython!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.pyCopied
🔗 Connecting to FastMCP server: ../gitHub_MCP_server/github_server.py✅ Client created successfully[06/28/25 09:51:48] INFO Starting MCP server 'GitHubMCP' with ]8;id=921531;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py\server.py]8;;\:]8;id=418078;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py#1246\1246]8;;\transport 'stdio'🛠️ Available tools (1):==================================================📋 list_more_repository_issuesDescription: Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issueParameters: owner, repo_name🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.💬 You can ask questions about GitHub repositories!📚 The client can use tools from the FastMCP server------------------------------------------------------------👤 You:
As before, there is no need to make a request, since it shows us the available tools and we can see that we have list_more_repository_issues.
🛠️ Available tools (1):
==================================================
📋 list_more_repository_issues
Description: Lists open issues for a given GitHub repository.
Args:
owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')
Returns:
list[dict]: Una lista de diccionarios, cada uno conteniendo información sobre un issue
Parameters: owner, repo_nameReturn to public
We set include_tags back to public to use the tool list_repository_issues
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="This server provides tools, resources and prompts to interact with the GitHub API.",include_tags={"public"})@mcp.tool(tags={"public", "production"})async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"private", "development"})async def list_more_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 100 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=100"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
Exclude tools by tags
Just as before we filtered the tools that can be used by tags, we can also exclude tools by tags. To do this, when creating the server, you need to add the exclude_tags parameter with the tags you want to exclude.
MCP Server
We create a new tool and exclude it using tags
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="This server provides tools, resources and prompts to interact with the GitHub API.",include_tags={"public"},exclude_tags={"first_issue"})@mcp.tool(tags={"public", "production"})async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"private", "development"})async def list_more_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 100 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=100"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"public", "first_issue"})async def first_repository_issue(owner: str, repo_name: str) -> list[dict]:"""Gets the first issue ever created in a GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list containing information about the first issue created"""# Get the first issue by sorting by creation date in ascending orderapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=all&sort=created&direction=asc&per_page=1"print(f"Fetching first issue from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No issues found for this repository.")return [{"message": "No issues found for this repository."}]first_issue = issues_data[0]# Create a detailed summary of the first issuesummary = f"#{first_issue.get('number', 'N/A')}: {first_issue.get('title', 'No title')}"if first_issue.get('comments', 0) > 0:summary += f" ({first_issue.get('comments')} comments)"issue_info = {"number": first_issue.get("number"),"title": first_issue.get("title"),"user": first_issue.get("user", {}).get("login"),"url": first_issue.get("html_url"),"state": first_issue.get("state"),"comments": first_issue.get("comments"),"created_at": first_issue.get("created_at"),"updated_at": first_issue.get("updated_at"),"body": first_issue.get("body", "")[:500] + "..." if len(first_issue.get("body", "")) > 500 else first_issue.get("body", ""),"summary": summary}print(f"Found first issue: #{first_issue.get('number')} created on {first_issue.get('created_at')}")# Add context informationresult = {"repository": f"{owner}/{repo_name}","note": "This is the very first issue created in this repository","first_issue": issue_info}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
We have created the tool first_repository_issue, but we won't be able to use it because it has the tags public and first_issue, but when creating the server we set exclude_tags={"first_issue"}.
exclude_tags test
We run the MCP client
InputPython!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.pyCopied
🔗 Connecting to FastMCP server: ../gitHub_MCP_server/github_server.py✅ Client created successfully[06/28/25 10:00:36] INFO Starting MCP server 'GitHubMCP' with ]8;id=28274;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py\server.py]8;;\:]8;id=529867;file:///Users/macm1/Documents/web/portafolio/posts/client_MCP/.venv/lib/python3.11/site-packages/fastmcp/server/server.py#1246\1246]8;;\transport 'stdio'🛠️ Available tools (1):==================================================📋 list_repository_issuesDescription: Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issueParameters: owner, repo_name🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.💬 You can ask questions about GitHub repositories!📚 The client can use tools from the FastMCP server------------------------------------------------------------👤 You:
We see that the tool first_repository_issue is not available
🛠️ Available tools (1):
==================================================
📋 list_repository_issues
Description: Lists open issues for a given GitHub repository.
Args:
owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')
Returns:
list[dict]: Una lista de diccionarios, cada uno conteniendo información sobre un issue
Parameters: owner, repo_nameServer Composition
Just as in programming you can inherit classes or build on already created functions, in MCP you can create sub-servers and compose them.
MCP Server
We are going to create an MCP subserver, with its own tool hello_world. Then we will mount it on the main server. By doing this, we will be able to use the tool hello_world in the client that connects to the main server.
InputPython%%writefile gitHub_MCP_server/github_server.pyimport httpxfrom typing import Optionalfrom fastmcp import FastMCPfrom github import GITHUB_TOKEN, create_github_headers# Create FastMCP servermcp = FastMCP(name="GitHubMCP",instructions="This server provides tools, resources and prompts to interact with the GitHub API.",include_tags={"public"},exclude_tags={"first_issue"})sub_mcp = FastMCP(name="SubMCP",)@mcp.tool(tags={"public", "production"})async def list_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 10 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=10"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"private", "development"})async def list_more_repository_issues(owner: str, repo_name: str) -> list[dict]:"""Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issue"""# Limit to first 100 issues to avoid very long responsesapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&per_page=100"print(f"Fetching issues from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No open issues found for this repository.")return [{"message": "No open issues found for this repository."}]issues_summary = []for issue in issues_data:# Create a more concise summarysummary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"if issue.get('comments', 0) > 0:summary += f" ({issue.get('comments')} comments)"issues_summary.append({"number": issue.get("number"),"title": issue.get("title"),"user": issue.get("user", {}).get("login"),"url": issue.get("html_url"),"comments": issue.get("comments"),"summary": summary})print(f"Found {len(issues_summary)} open issues.")# Add context informationresult = {"total_found": len(issues_summary),"repository": f"{owner}/{repo_name}","note": "Showing first 10 open issues" if len(issues_summary) == 10 else f"Showing all {len(issues_summary)} open issues","issues": issues_summary}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@mcp.tool(tags={"public", "first_issue"})async def first_repository_issue(owner: str, repo_name: str) -> list[dict]:"""Gets the first issue ever created in a GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list containing information about the first issue created"""# Get the first issue by sorting by creation date in ascending orderapi_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=all&sort=created&direction=asc&per_page=1"print(f"Fetching first issue from {api_url}...")async with httpx.AsyncClient() as client:try:response = await client.get(api_url, headers=create_github_headers())response.raise_for_status()issues_data = response.json()if not issues_data:print("No issues found for this repository.")return [{"message": "No issues found for this repository."}]first_issue = issues_data[0]# Create a detailed summary of the first issuesummary = f"#{first_issue.get('number', 'N/A')}: {first_issue.get('title', 'No title')}"if first_issue.get('comments', 0) > 0:summary += f" ({first_issue.get('comments')} comments)"issue_info = {"number": first_issue.get("number"),"title": first_issue.get("title"),"user": first_issue.get("user", {}).get("login"),"url": first_issue.get("html_url"),"state": first_issue.get("state"),"comments": first_issue.get("comments"),"created_at": first_issue.get("created_at"),"updated_at": first_issue.get("updated_at"),"body": first_issue.get("body", "")[:500] + "..." if len(first_issue.get("body", "")) > 500 else first_issue.get("body", ""),"summary": summary}print(f"Found first issue: #{first_issue.get('number')} created on {first_issue.get('created_at')}")# Add context informationresult = {"repository": f"{owner}/{repo_name}","note": "This is the very first issue created in this repository","first_issue": issue_info}return [result]except httpx.HTTPStatusError as e:error_message = e.response.json().get("message", "No additional message from API.")if e.response.status_code == 403 and GITHUB_TOKEN:error_message += " (Rate limit with token or token lacks permissions?)"elif e.response.status_code == 403 and not GITHUB_TOKEN:error_message += " (Rate limit without token. Consider creating a .env file with GITHUB_TOKEN.)"print(f"GitHub API error: {e.response.status_code}. {error_message}")return [{"error": f"GitHub API error: {e.response.status_code}","message": error_message}]except Exception as e:print(f"An unexpected error occurred: {str(e)}")return [{"error": f"An unexpected error occurred: {str(e)}"}]@sub_mcp.tool(tags={"public"})def hello_world() -> str:"""Returns a simple greeting."""return "Hello, world!"mcp.mount("sub_mcp", sub_mcp)if __name__ == "__main__":print("DEBUG: Starting FastMCP GitHub server...")print(f"DEBUG: Server name: {mcp.name}")# Initialize and run the servermcp.run()Copied
Overwriting gitHub_MCP_server/github_server.py
MCP servers composition test
We run the client
InputPython!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.pyCopied
🔗 Connecting to FastMCP server: ../gitHub_MCP_server/github_server.py✅ Client created successfully/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server/github_server.py:240: DeprecationWarning: Mount prefixes are now optional and the first positional argument should be the server you want to mount.mcp.mount("sub_mcp", sub_mcp)[06/28/25 10:10:58] INFO Starting MCP server 'GitHubMCP' with transport 'stdio' server.py:1246🛠️ Available tools (2):==================================================📋 sub_mcp_hello_worldDescription: Returns a simple greeting.Parameters:📋 list_repository_issuesDescription: Lists open issues for a given GitHub repository.Args:owner: The owner of the repository (e.g., 'modelcontextprotocol')repo_name: The name of the repository (e.g., 'python-sdk')Returns:list[dict]: A list of dictionaries, each containing information about an issueParameters: owner, repo_name🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.💬 You can ask questions about GitHub repositories!📚 The client can use tools from the FastMCP server------------------------------------------------------------👤 You: Can you greeting me?🤔 Claude is thinking...🔧 Claude wants to use: sub_mcp_hello_world📝 Arguments: {}✅ Tool executed successfully🤖 Claude: I'll help you send a greeting using the `sub_mcp_hello_world` function. This function returns a simple greeting.There's your greeting! The function returned "Hello, world!"👤 You: q👋 Bye!🧹 Cleaning up resources...✅ Resources released
We can see that the new tool sub_mcp_hello_world has appeared
🛠️ Available tools (2):
==================================================
📋 sub_mcp_hello_world
Description: Returns a simple greeting.
Parameters:
📋 list_repository_issues
Description: Lists open issues for a given GitHub repository.
Args:
owner: The owner of the repository (e.g., 'modelcontextprotocol')
repo_name: The name of the repository (e.g., 'python-sdk')
Returns:
list[dict]: Una lista de diccionarios, cada uno contiene información sobre un issue
Parameters: owner, repo_nameAnd when we ask it to greet us, it executes it
👤 You: Can you greet me?
🤔 Claude is thinking...
🔧 Claude wants to use: sub_mcp_hello_world
📝 Arguments: {}✅ Tool executed successfully
🤖 Claude: Te ayudaré a enviar un saludo usando la función `sub_mcp_hello_world`. Esta función devuelve un saludo simple.¡Aquí tienes tu saludo! La función devolvió "Hello, world!"---
➡️ **Continue in Part 2: transport layer, context, and resources**.