📚 **Esta entrada es parte de la serie _MCP con FastMCP_**, dividida en cuatro capítulos que se leen en orden:
> * 👉 **Parte 1: Primer servidor y tools**
* Parte 2: Transporte, contexto y resources
* Parte 3: Resources avanzados y prompts
* Parte 4: HTTP, autenticación y cliente
¿Qué es MCP?
MCP (Model Context Protocol) es un estándar open source desarrollado por Anthropic para permitir a los modelos de IA interactuar con herramientas externas mediante un estándar
Hasta el desarrollo del protocolo MCP, cuando queríamos que un LLM interactuara con herramientas, teníamos que crear código para poder interactuar con la herramienta, y mediante function calling enviarle la información al LLM.
Así que, mediante MCP, un LLM puede interactuar con herramientas gracias a un estándar. De esta manera, si una persona crea un servidor MCP, dicho servidor puede ser reutilizado por otros con un único cliente. Si en tu aplicación desarrollas un cliente, puedes descargarte un servidor MCP desarrollado por otro y usarlo sin problema.
Comúnmente MCP se asemeja al estándar USB. Antes del USB, cada periférico tenía un tipo de conexión diferente, unos tenían puertos serie, otros paralelo. Diferentes formatos de conectores, etc.
Con la llegada del USB, todos los periféricos se adaptaron a este estándar, por lo que con un solo conector USB en tu ordenador, puedes conectar casi cualquier periférico.
MCP tiene 7 componentes principales:
- Host: Aplicación LLM que tiene acceso a herramientas MCP.
- Servidor MCP: Servidor que realiza la comunicación con la API o herramienta a la que queremos exponer al LLM
- Cliente MCP: Cliente que se conecta al servidor MCP y realiza las peticiones
- Tool: Función que se ejecuta en el servidor MCP y que puede ser invocada por el LLM
- Resource: Recurso que se puede usar en el servidor MCP. Suelen dar al LLM acceso a recursos estáticos como archivos, bases de datos, etc.
- Resource template: Template para crear recursos dinámicos. Mediante estas plantillas, el LLM puede crear dinámicamente el recurso al que quiere acceder
- Prompt: Prompt que se usa para generar un prompt que será usado por el LLM para interactuar con el servidor MCP.
Un único host (aplicación) puede tener varios clientes. Cada cliente se conectará a un servidor MCP
FastMCP
Aunque en la documentación de MCP recomiendan instalar mcp["cli"], hay una librería creada por encima llamada fastmcp, que ayuda mucho a la hora de crear servidores MCP, así que vamos a usarla
Crear entorno virtual
Para crear un servidor y un cliente MCP, vamos a crear entornos virtuales con uv con las dependencias que vamos a necesitar
Servidor MCP
Primero creamos una carpeta para el servidor de MCP
InputPython!mkdir gitHub_MCP_serverCopied
Iniciamos el entorno uv
InputPython!cd gitHub_MCP_server && uv init .Copied
Initialized project `github-mcp-server` at `/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server`
Lo activamos
InputPython!cd gitHub_MCP_server && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
E instalamos las librerías necesarias
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
Cliente MCP
Ahora creamos una carpeta donde programaremos el cliente MCP
InputPython!mkdir client_MCPCopied
Iniciamos el entorno uv
InputPython!cd client_MCP && uv init .Copied
Initialized project `client-mcp` at `/Users/macm1/Documents/web/portafolio/posts/client_MCP`
Lo activamos
InputPython!cd client_MCP && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
Y por último, instalamos las librerías necesarias para el cliente.
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
Vamos a usar Sonnet 3.5 como modelo LLM, así que creamos un archivo .env en la carpeta del cliente con la API Key de Claude que se puede obtener en la página keys de la API de Claude
InputPython%%writefile client_MCP/.envANTHROPIC_API_KEY="ANTHROPIC_API_KEY"Copied
Writing client_MCP/.env
MCP básico
Escribimos el mínimo código que necesitamos para tener un servidor MCP
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
Como se puede ver, tenemos que crear un objeto FastMCP y luego ejecutar el servidor con mcp.run.
Librería con funciones para leer de GitHub
Como vamos a crear un servidor MCP para poder usar utilidades de GitHub, vamos a crear un archivo con las funciones necesarias para construir los headers necesarios para poder usar la API de GitHub.
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
Para poder construir los headers, necesitamos un token de GitHub. Para ello, vamos a personal-access-tokens y creamos un nuevo token. Lo copiamos
Ahora, creamos un .env, donde vamos a almacenar el token de GitHub.
InputPython%%writefile gitHub_MCP_server/.envGITHUB_TOKEN = "GITHUB_TOKEN"Copied
Overwriting gitHub_MCP_server/.env
Crear tool de MCP para obtener una lista de issues de un repositorio de GitHub
Servidor MCP
Añadimos una función para poder listar los issues de un repositorio de GitHub. Para convertir dicha función en una tool de MCP, usamos el decorador @mcp.tool()
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
Cliente MCP
Ahora creamos un cliente MCP para poder usar la tool que hemos creado
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
Explicación del cliente MCP
- En
mainse comprueba que se ha pasado un argumento con el path del servidor MCP. - Se crea un objeto de la clase
FastMCPClientcon el path del servidor MCP. Al crear el objeto se ejecuta el método__init__que crea la conexión con el LLM de Anthropic, que va a ser el LLM que va a poner el "cerebro" - Se intenta conectar con el servidor MCP llamando al método
connect_to_serverpara abrir una sesión con el servidor MCP. - Se listan las
tools disponibles con el métodolist_available_tools - Si se ha podido conectar, se llama al método
chat_loopque es un bucle infinito para chatear con el LLM que se acaba de crear en el cliente. Solo se para la ejecución cuando se introducequit,q,exitosaliren el chat. - Se procesa la entrada del usuario con el método
process_queryque obtiene la lista detools disponibles y hace una petición al LLM con el mensaje del usuario y la lista detools - Si el LLM responde con texto, se devuelve el texto, que será impreso
- Si el LLM responde con
tool_use, se obtiene el nombre de latool, los argumentos y se crea un ID de ejecución. Se ejecuta la tool. Con el resultado de la tool, se crea un nuevo mensaje que se le manda al LLM para que lo procese y genere una respuesta, que será devuelta e impresa. - Cuando se termine la conversación, se llamará al método
cleanup, que cerrará lo que sea necesario cerrar.
Prueba de la tool
Nos vamos a la ruta del cliente y lo ejecutamos, dándole la ruta del servidor MCP.
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
Al ejecutarlo vemos
🛠️ 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]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_nameLo que indica que el cliente MCP puede ver la tool que hemos creado en el servidor MCP.
Después podemos ver
👤 You: Tell me de issues of repository transformers of huggingface
🤔 Claude is thinking...
🔧 Calling tool: list_repository_issues
📝 Arguments: {'owner': 'huggingface', 'repo_name': 'transformers'}
✅ Tool executed successfullyLe pedimos los issues del repositorio transformers de huggingface. Tras pensar un rato nos dice que va a usar la tool list_repository_issues con los argumentos {'owner': 'huggingface', 'repo_name': 'transformers'}.
Por último, nos dice que la tool se ha ejecutado correctamente.
Por último, con el resultado de ejecutar la tool, Claude lo procesa y nos crea una respuesta con la lista de issues.
🤖 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 dispatched
2. [#39096] Pull request to fix 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] Style change PR for lru_cache decorator
6. [#39091] Compatibility issue with sentencepiece on Windows in Python 3.13
7. [#39090] Pull request for fixing bugs in finetune and batch inference
8. [#39089] Bug report for LlavaOnevisonConfig initialization in version 4.52.4
9. [#39087] Documentation PR for Gemma 3n audio encoder
10. [#39084] Pull request for refactoring gemma3n
Note 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?Crear el servidor MCP con más información
Servidor MCP
Antes hemos creado el servidor con mcp = FastMCP(), pero podemos aprovechar para darle un nombre y descripción al servidor con
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
Filtrar tools mediante tags
Servidor MCP
MCP nos da la opción de poder exponer tools mediante tags, lo cual puede ser útil para exponer solo tools para depuración, para que solo las puedan usar determinados usuarios, etc.
Para ello, cuando creamos el servidor MCP indicamos los tags que queremos incluir
mcp = FastMCP(
name="GitHubMCP",
instructions="This server provides tools, resources and prompts to interact with the GitHub API.",
include_tags={"public"}
)Y luego, cuando creamos la tool podemos indicar los tags que queremos que tenga.
@mcp.tool(tags=production)Vamos a ver un ejemplo
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
Podemos ver que hemos creado la función list_repository_issues, que lista solo 10 issues y que tiene los tags public y production. Y hemos creado la función list_more_repository_issues, que lista 100 issues de un repositorio y que tiene los tags private y development.
Además hemos declarado el servidor mediante
mcp = FastMCP(
name="GitHubMCP",
instructions="This server provides tools, resources and prompts to interact with the GitHub API.",
include_tags={"public"}
)Por lo que el cliente solo tendrá acceso a las tools que tengan el tag public, es decir, a list_repository_issues. Solo va a poder ver una lista de 10 issues.
Prueba de los tags
Volvemos a ejecutar el cliente MCP
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:
No hace falta hacer una petición, ya que vemos lo siguiente:
🛠️ 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: The MCP context for logging.
Returns:
list[dict]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_nameEs decir, el cliente solo puede ver la tool list_repository_issues y no la tool list_all_repository_issues.
Cambio a private
Cambiamos include_tags a private para usar la 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
Prueba del tag private
Ejecutamos otra vez el cliente con el cambio hecho
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:
Al igual que antes, no hace falta hacer una petición, ya que nos muestra las tools disponibles y vemos que tenemos 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]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_nameVuelta a public
Volvemos a poner include_tags a public para usar la 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
Excluir tools por tags
Al igual que antes hemos filtrado las tools que se pueden usar por tags, también podemos excluir tools por tags, para ello, a la hora de crear el servidor hay que añadir el parámetro exclude_tags con los tags que queremos excluir.
Servidor MCP
Creamos una nueva tool y la excluimos mediante 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
Hemos creado la tool first_repository_issue, pero no la vamos a poder usar porque tiene los tags public y first_issue, pero a la hora de crear el servidor hemos puesto exclude_tags={"first_issue"}.
Prueba de exclude_tags
Ejecutamos el cliente MCP
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:
Vemos que no está disponible la tool first_repository_issue
🛠️ 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]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_nameComposición de servidores
Al igual que en programación se pueden heredar clases, o construir sobre funciones ya creadas, en MCP se pueden crear sub-servidores y crear una composición de ellos.
Servidor MCP
Vamos a crear un subservidor MCP, con su propia tool hello_world. Después lo montamos en el servidor principal. Haciendo esto, vamos a poder usar la tool hello_world en el cliente que se conecte al servidor principal.
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
Prueba de la composición de servidores MCP
Ejecutamos el cliente
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
Podemos ver que ha aparecido la nueva tool sub_mcp_hello_world
🛠️ 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]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_nameY cuando le pedimos que nos salude la ejecuta
👤 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!"---
➡️ **Continúa en la Parte 2: capa de transporte, contexto y resources**.