MCP con FastMCP (1/4): crea tu primer servidor y gestiona sus tools

MCP con FastMCP (1/4): crea tu primer servidor y gestiona sus tools

📚 **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?link image 1

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.

MCP vs API

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.

USB MCP

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

mcp architecture

FastMCPlink image 2

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 virtuallink image 3

Para crear un servidor y un cliente MCP, vamos a crear entornos virtuales con uv con las dependencias que vamos a necesitar

Servidor MCPlink image 4

Primero creamos una carpeta para el servidor de MCP

	
< > Input
Python
!mkdir gitHub_MCP_server
Copied

Iniciamos el entorno uv

	
< > Input
Python
!cd gitHub_MCP_server && uv init .
Copied
>_ Output
			
Initialized project `github-mcp-server` at `/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server`

Lo activamos

	
< > Input
Python
!cd gitHub_MCP_server && uv venv
Copied
>_ Output
			
Using CPython 3.11.11
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

E instalamos las librerías necesarias

	
< > Input
Python
!cd gitHub_MCP_server && uv add anthropic fastmcp python-dotenv requests
Copied
>_ Output
			
Resolved 42 packages in 34ms
Installed 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 MCPlink image 5

Ahora creamos una carpeta donde programaremos el cliente MCP

	
< > Input
Python
!mkdir client_MCP
Copied

Iniciamos el entorno uv

	
< > Input
Python
!cd client_MCP && uv init .
Copied
>_ Output
			
Initialized project `client-mcp` at `/Users/macm1/Documents/web/portafolio/posts/client_MCP`

Lo activamos

	
< > Input
Python
!cd client_MCP && uv venv
Copied
>_ Output
			
Using CPython 3.11.11
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

Y por último, instalamos las librerías necesarias para el cliente.

	
< > Input
Python
!cd client_MCP && uv add anthropic fastmcp python-dotenv requests
Copied
>_ Output
			
Resolved 42 packages in 307ms
Prepared 5 packages in 115ms
Installed 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

	
< > Input
Python
%%writefile client_MCP/.env
ANTHROPIC_API_KEY="ANTHROPIC_API_KEY"
Copied
>_ Output
			
Writing client_MCP/.env

MCP básicolink image 6

Escribimos el mínimo código que necesitamos para tener un servidor MCP

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("GitHubMCP")
if __name__ == "__main__":
# Initialize and run the server
mcp.run(transport='stdio')
Copied
>_ Output
			
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 GitHublink image 7

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.

	
< > Input
Python
%%writefile gitHub_MCP_server/github.py
import os
from dotenv import load_dotenv
# Load the GitHub token from the .env file
load_dotenv()
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
# Check if the GitHub token is configured
if 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 requests
def create_github_headers():
headers = {}
if GITHUB_TOKEN:
headers["Authorization"] = f"Bearer {GITHUB_TOKEN}"
# GitHub recommends including a User-Agent
headers["User-Agent"] = "MCP_GitHub_Server_Example"
headers["Accept"] = "application/vnd.github.v3+json" # Good practice
return headers
Copied
>_ Output
			
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.

	
< > Input
Python
%%writefile gitHub_MCP_server/.env
GITHUB_TOKEN = "GITHUB_TOKEN"
Copied
>_ Output
			
Overwriting gitHub_MCP_server/.env

Crear tool de MCP para obtener una lista de issues de un repositorio de GitHublink image 8

Servidor MCPlink image 9

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()

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create a FastMCP server
mcp = FastMCP("GitHubMCP")
@mcp.tool()
async def list_repository_issues(owner: str, repo_name: str) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'Sin título')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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 server
mcp.run()
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Cliente MCPlink image 10

Ahora creamos un cliente MCP para poder usar la tool que hemos creado

	
< > Input
Python
%%writefile client_MCP/client.py
import sys
import asyncio
from contextlib import AsyncExitStack
from anthropic import Anthropic
from dotenv import load_dotenv
from fastmcp import Client
# Load environment variables from .env file
load_dotenv()
class FastMCPClient:
"""
FastMCP client that integrates with Claude to process user queries
and 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 = None
async 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 extension
if not server_script_path.endswith('.py'):
raise ValueError(f"Unsupported server type. Use .py files. Received: {server_script_path}")
# Create FastMCP client
self.client = Client(server_script_path)
# Note: FastMCP Client automatically infers transport from .py files
print("✅ 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 context
async 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 available
if 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) -&gt; str:
"""
Process a user query, interacting with Claude and FastMCP tools.
Args:
query: User query
Returns:
str: Final processed response
"""
try:
# Use FastMCP context for all operations
async with self.client as client:
# Get available tools
tools_list = await client.list_tools()
# Prepare tools for Claude in correct format
claude_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 Claude
messages = [
{
"role": "user",
"content": query
}
]
# First call to Claude
response = 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 response
response_text = ""
for content_block in response.content:
if content_block.type == "text":
response_text += content_block.text
elif content_block.type == "tool_use":
# Claude wants to use a tool
tool_name = content_block.name
tool_args = content_block.input
tool_call_id = content_block.id
print(f"🔧 Claude wants to use: {tool_name}")
print(f"📝 Arguments: {tool_args}")
try:
# Execute tool on the FastMCP server
tool_result = await client.call_tool(tool_name, tool_args)
print(f"✅ Tool executed successfully")
# Add tool result to the conversation
messages.append({
"role": "assistant",
"content": response.content
})
# Format result for Claude
if tool_result:
# Convert result to string format for Claude
result_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 result
final_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 response
for final_content in final_response.content:
if final_content.type == "text":
response_text += final_content.text
except Exception as e:
error_msg = f"❌ Error executing {tool_name}: {str(e)}"
print(error_msg)
response_text += f" {error_msg}"
return response_text
except Exception as e:
error_msg = f"❌ Error processing query: {str(e)}"
print(error_msg)
return error_msg
async 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 input
user_input = input(" 👤 You: ").strip()
if user_input.lower() in ['quit', 'q', 'exit', 'salir']:
print("👋 Bye!")
break
if not user_input:
continue
print(" 🤔 Claude is thinking...")
# Process query
response = await self.process_query(user_input)
# Show response
print(f" 🤖 Claude: {response}")
except KeyboardInterrupt:
print(" 👋 Disconnecting...")
break
except Exception as e:
print(f" ❌ Error in chat: {str(e)}")
continue
async def cleanup(self):
"""Clean up resources and close connections."""
print("🧹 Cleaning up resources...")
# FastMCP Client cleanup is handled automatically by context manager
await self.exit_stack.aclose()
print("✅ Resources released")
async def main():
"""
Main function that initializes and runs the FastMCP client.
"""
# Verify command line arguments
if len(sys.argv) != 2:
print("❌ Usage: python client.py &lt;path_to_fastmcp_server&gt;")
print("📝 Example: python client.py ../MCP_github/github_server.py")
sys.exit(1)
server_script_path = sys.argv[1]
# Create and run client
client = FastMCPClient()
try:
# Connect to the server
await client.connect_to_server(server_script_path)
# List available tools after connection
await client.list_available_tools()
# Start chat loop
await client.chat_loop()
except Exception as e:
print(f"❌ Fatal error: {str(e)}")
finally:
# Ensure resources are cleaned up
await client.cleanup()
if __name__ == "__main__":
# Entry point of the script
asyncio.run(main())
Copied
>_ Output
			
Overwriting client_MCP/client.py

Explicación del cliente MCP

  • En main se comprueba que se ha pasado un argumento con el path del servidor MCP.
  • Se crea un objeto de la clase FastMCPClient con 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_server para abrir una sesión con el servidor MCP.
  • Se listan las tools disponibles con el método list_available_tools
  • Si se ha podido conectar, se llama al método chat_loop que 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 introduce quit, q, exit o salir en el chat.
  • Se procesa la entrada del usuario con el método process_query que obtiene la lista de tools disponibles y hace una petición al LLM con el mensaje del usuario y la lista de tools
    • 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 la tool, 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 toollink image 11

Nos vamos a la ruta del cliente y lo ejecutamos, dándole la ruta del servidor MCP.

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.py
Copied
>_ Output
			
🔗 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_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_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 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?
👤 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_name

Lo 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 successfully

Le 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ónlink image 12

Servidor MCPlink image 13

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.
"""
)
	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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 server
mcp.run()
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Filtrar tools mediante tagslink image 14

Servidor MCPlink image 15

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

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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 server
mcp.run()
Copied
>_ Output
			
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 tagslink image 16

Volvemos a ejecutar el cliente MCP

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.py
Copied
>_ Output
			
🔗 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_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_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_name

Es decir, el cliente solo puede ver la tool list_repository_issues y no la tool list_all_repository_issues.

Cambio a privatelink image 17

Cambiamos include_tags a private para usar la tool list_more_repository_issues

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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 server
mcp.run()
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Prueba del tag privatelink image 18

Ejecutamos otra vez el cliente con el cambio hecho

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.py
Copied
>_ Output
			
🔗 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_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_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_name

Vuelta a publiclink image 19

Volvemos a poner include_tags a public para usar la tool list_repository_issues

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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 server
mcp.run()
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Excluir tools por tagslink image 20

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 MCPlink image 21

Creamos una nueva tool y la excluimos mediante tags

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 order
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=all&amp;sort=created&amp;direction=asc&amp;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 issue
summary = f"#{first_issue.get('number', 'N/A')}: {first_issue.get('title', 'No title')}"
if first_issue.get('comments', 0) &gt; 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", "")) &gt; 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 information
result = {
"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 server
mcp.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_tagslink image 22

Ejecutamos el cliente MCP

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.py
Copied
>_ Output
			
🔗 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_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_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_name

Composición de servidoreslink image 23

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 MCPlink image 24

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.

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from typing import Optional
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
# Create FastMCP server
mcp = 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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 responses
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=open&amp;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 summary
summary = f"#{issue.get('number', 'N/A')}: {issue.get('title', 'No title')}"
if issue.get('comments', 0) &gt; 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 information
result = {
"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) -&gt; 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 order
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues?state=all&amp;sort=created&amp;direction=asc&amp;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 issue
summary = f"#{first_issue.get('number', 'N/A')}: {first_issue.get('title', 'No title')}"
if first_issue.get('comments', 0) &gt; 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", "")) &gt; 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 information
result = {
"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() -&gt; 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 server
mcp.run()
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Prueba de la composición de servidores MCPlink image 25

Ejecutamos el cliente

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py ../gitHub_MCP_server/github_server.py
Copied
>_ Output
			
🔗 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_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_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_name

Y 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**.

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
React Native
Expo
TypeScript
FastAPI
Next.js
OpenAI
Anthropic

Aplicación móvil de entrenamiento personal con asistente de IA, biblioteca de ejercicios, seguimiento de rutinas, dieta y medidas corporales

Horeca chatbot

Horeca chatbot Horeca chatbot
Python
LangChain
PostgreSQL
PGVector
React
Kubernetes
Docker
GitHub Actions

Chatbot conversacional para cocineros de hoteles y restaurantes. Un cocinero, jefe de cocina o camaeror de un hotel o restaurante puede hablar con el chatbot para obtener información de recetas y menús. Pero además implementa agentes, con los cuales puede editar o crear nuevas recetas o menús

Naviground

Naviground Naviground
Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->