Aviso: Este post foi traduzido para o português usando um modelo de tradução automática. Por favor, me avise se encontrar algum erro.
📚 **Esta entrada faz parte da série _MCP com FastMCP_**, dividida em quatro capítulos que se leem em ordem:
> * 👉 **Parte 1: Primeiro servidor e tools**
* Parte 2: Transporte, contexto y resources
* Parte 3: Recursos avançados e prompts
* Parte 4: HTTP, autenticação y cliente
O que é MCP?
MCP (Model Context Protocol) é um padrão open source desenvolvido pela Anthropic para permitir que os modelos de IA interajam com ferramentas externas por meio de um padrão
Até o desenvolvimento do protocolo MCP, quando queríamos que um LLM interagisse com ferramentas, tínhamos que criar código para poder interagir com a ferramenta e, por meio de function calling, enviar a informação ao LLM.
Assim, por meio do MCP, um LLM pode interagir com ferramentas graças a um padrão. Dessa forma, se uma pessoa cria um servidor MCP, esse servidor pode ser reutilizado por outros com um único cliente. Se na tua aplicação desenvolves um cliente, podes descarregar um servidor MCP desenvolvido por outro e usá-lo sem problema.
Comumente, o MCP se assemelha ao padrão USB. Antes do USB, cada periférico tinha um tipo de conexão diferente: alguns tinham portas seriais, outros paralelas. Diferentes formatos de conectores, etc.
Com a chegada do USB, todos os periféricos se adaptaram a esse padrão, de modo que, com um único conector USB no teu computador, podes conectar quase qualquer periférico.
O MCP tem 7 componentes principais:
- Host: Aplicação LLM que tem acesso a ferramentas MCP.
- Servidor MCP: Servidor que realiza a comunicação com a API ou ferramenta que queremos expor ao LLM
- Cliente MCP: Cliente que se conecta ao servidor MCP e realiza as solicitações
- Ferramenta: Função que é executada no servidor MCP e que pode ser invocada pelo LLM
- Recurso: Recurso que pode ser usado no servidor MCP. Geralmente concedem ao LLM acesso a recursos estáticos, como arquivos, bancos de dados, etc.
- Resource template: Template para criar recursos dinâmicos. Através desses modelos, o LLM pode criar dinamicamente o recurso ao qual deseja acessar
- Prompt: Prompt usado para gerar um prompt que será usado pelo LLM para interagir com o servidor MCP.
Um único host (aplicação) pode ter vários clientes. Cada cliente se conectará a um servidor MCP
FastMCP
Embora na documentação do MCP recomendem instalar mcp["cli"], há uma biblioteca criada por cima chamada fastmcp, que ajuda muito na hora de criar servidores MCP, então vamos usá-la
Criar ambiente virtual
Para criar um servidor e um cliente MCP, vamos criar ambientes virtuais com uv com as dependências de que vamos precisar
Servidor MCP
Primeiro, criamos uma pasta para o servidor MCP
InputPython!mkdir gitHub_MCP_serverCopied
Iniciamos o ambiente uv
InputPython!cd gitHub_MCP_server && uv init .Copied
Initialized project `github-mcp-server` at `/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server`
Ativamos isso
InputPython!cd gitHub_MCP_server && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
E instalamos as bibliotecas necessárias
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
Agora criamos uma pasta onde programaremos o cliente MCP
InputPython!mkdir client_MCPCopied
Iniciamos o ambiente uv
InputPython!cd client_MCP && uv init .Copied
Initialized project `client-mcp` at `/Users/macm1/Documents/web/portafolio/posts/client_MCP`
Nós ativamos isso
InputPython!cd client_MCP && uv venvCopied
Using CPython 3.11.11Creating virtual environment at: .venvActivate with: source .venv/bin/activate
E, por último, instalamos as bibliotecas necessárias para o 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 usar o Sonnet 3.5 como modelo LLM, então criamos um arquivo .env na pasta do cliente com a API Key do Claude que pode ser obtida na página keys da API do Claude
InputPython%%writefile client_MCP/.envANTHROPIC_API_KEY="ANTHROPIC_API_KEY"Copied
Writing client_MCP/.env
MCP básico
Escrevemos o mínimo código de que precisamos para ter um 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 pode ver, temos que criar um objeto FastMCP e depois executar o servidor com mcp.run.
Biblioteca com funções para ler do GitHub
Como vamos a criar um servidor MCP para poder usar utilidades do GitHub, vamos criar um arquivo com as funções necessárias para construir os headers necessários para poder usar a API do 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 os headers, precisamos de um token do GitHub. Para isso, vamos a personal-access-tokens e criamos um novo token. Copiamo-lo
Agora, criamos um .env, onde vamos armazenar o token do GitHub.
InputPython%%writefile gitHub_MCP_server/.envGITHUB_TOKEN = "GITHUB_TOKEN"Copied
Overwriting gitHub_MCP_server/.env
Criar tool de MCP para obter uma lista de issues de um repositório do GitHub
Servidor MCP
Adicionamos uma função para poder listar os issues de um repositório do GitHub. Para converter essa função em uma tool de MCP, usamos o 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
Agora criamos um cliente MCP para poder usar a tool que criamos
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
Explicação do cliente MCP
- Em
mainverifica-se que foi passado um argumento com o caminho do servidor MCP. - Cria-se um objeto da classe
FastMCPClientcom o caminho do servidor MCP. Ao criar o objeto, é executado o método__init__, que cria a conexão com o LLM da Anthropic, que será o LLM que vai colocar o "cérebro" - Tenta-se conectar com o servidor MCP chamando o método
connect_to_serverpara abrir uma sessão com o servidor MCP. - As
tools disponíveis são listadas com o métodolist_available_tools - Se foi possível conectar, chama-se o método
chat_loop, que é um loop infinito para conversar com o LLM que acabou de ser criado no cliente. A execução só é interrompida quando se introduzquit,q,exitousalirno chat. - A entrada do usuário é processada com o método
process_query, que obtém a lista detools disponíveis e faz uma solicitação ao LLM com a mensagem do usuário e a lista detools - Se o LLM responder com texto, o texto é retornado, que será impresso
- Se o LLM responder com
tool_use, obtém-se o nome datool, os argumentos e cria-se um ID de execução. A ferramenta é executada. Com o resultado da ferramenta, cria-se uma nova mensagem que é enviada ao LLM para que a processe e gere uma resposta, que será devolvida e impressa. - Quando a conversa terminar, o método
cleanupserá chamado, que fechará tudo o que for necessário fechar.
Teste da tool
Vamos para a rota do cliente e o executamos, fornecendo a rota do 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
Ao executá-lo, vemos
🛠️ Ferramentas disponíveis (1):
==================================================
📋 listar_issues_do_repositório
Descrição: Lista os problemas abertos de um determinado repositório do GitHub.
Args:
owner: O proprietário do repositório (por exemplo, 'modelcontextprotocol')
repo_name: O nome do repositório (por exemplo, 'python-sdk')
Retornos:
list[dict]: Uma lista de dicionários, cada um contendo informações sobre um problema
Parâmetros: owner, repo_nameO que indica que o cliente MCP pode ver a tool que criamos no servidor MCP.
Depois podemos ver
👤 Você: Me conte os problemas do repositório transformers da huggingface
🤔 Claude está pensando...
🔧 Chamando ferramenta: list_repository_issues
📝 Argumentos: {'owner': 'huggingface', 'repo_name': 'transformers'}
✅ Ferramenta executada com sucessoPedimos os issues do repositório transformers da huggingface. Após pensar um pouco, nos diz que vai usar a tool list_repository_issues com os argumentos {'owner': 'huggingface', 'repo_name': 'transformers'}.
Por fim, ele nos diz que a tool foi executada corretamente.
Por fim, com o resultado de executar a tool, Claude o processa e nos cria uma resposta com a lista de issues.
🤖 Claude: Vou ajudar você a listar os problemas do repositório Hugging Face transformers. Vou usar a função `list_repository_issues` com os parâmetros apropriados. Vou resumir os problemas abertos atuais do repositório Hugging Face transformers. Aqui estão os 10 problemas abertos mais recentes:
1. [#39097] Problema central sobre salvar modelos com múltiplos grupos de tensores compartilhados quando despachados
2. [#39096] Pull request para corrigir o índice de posição no v4.52.4
3. [#39095] Problema con Qwen2_5_VLVisionAttention: falta el atributo `is_causal` en flash attention
4. [#39094] Melhoria da documentação para exemplos do PyTorch
5. [#39093] PR de alteração de estilo para o decorador lru_cache
6. [#39091] Problema de compatibilidade com sentencepiece no Windows em Python 3.13
7. [#39090] Pull request para corrigir bugs em fine-tuning e inferência em lote8. [#39089] Informe de error sobre la inicialización de LlavaOnevisonConfig en la versión 4.52.4
9. [#39087] PR de documentação para o codificador de áudio do Gemma 3n
10. [#39084] Pull request para refatoração do gemma3n
Observe que isto mostra os 10 problemas em aberto mais recentes, e pode haver mais problemas no repositório. Cada problema tem um link onde você pode encontrar mais detalhes sobre o problema específico ou as alterações propostas.
Gostaria de mais informações específicas sobre algum destes problemas?Criar o servidor MCP com mais informações
Servidor MCP
Antes, criamos o servidor com mcp = FastMCP(), mas podemos aproveitar para dar a ele um nome e uma descrição com o servidor com
mcp = FastMCP(
name="GitHubMCP",
instructions="""
Este servidor fornece ferramentas, recursos e prompts para interagir com a API do GitHub.markdown
"""
)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 dá a opção de expor tools por meio de tags, o que pode ser útil para expor apenas tools para depuração, para que somente determinados usuários possam usá-las, etc.
Para isso, quando criamos o servidor MCP, indicamos as tags que queremos incluir
mcp = FastMCP(
name="GitHubMCP",
instructions="Este servidor fornece ferramentas, recursos e prompts para interagir com a API do GitHub.",
include_tags={"public"}
)E depois, quando criamos a tool, podemos indicar as tags que queremos que ela tenha.
@mcp.tool(tags=production)Vamos ver um exemplo
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 criamos a função list_repository_issues, que lista apenas 10 issues e que tem as tags public e production. E criamos a função list_more_repository_issues, que lista 100 issues de um repositório e que tem as tags private e development.
Além disso, declaramos o servidor por meio de
mcp = FastMCP(
name="GitHubMCP",
instructions="Este servidor fornece ferramentas, recursos e prompts para interagir com a API do GitHub.",
include_tags={"public"})Portanto, o cliente só terá acesso às tools que tenham a tag public, ou seja, a list_repository_issues. Só poderá ver uma lista de 10 issues.
Teste das tags
Voltamos a executar o 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:
Não é necessário fazer uma solicitação, pois vemos o seguinte:
🛠️ Ferramentas disponíveis (1):
==================================================
📋 list_repository_issues
Description: Lista problemas abertos para um determinado repositório do GitHub.
Args:
owner: O proprietário do repositório (por exemplo, 'modelcontextprotocol')
repo_name: O nome do repositório (por exemplo, 'python-sdk')
ctx: O contexto MCP para logging.
Retorna:
list[dict]: Uma lista de dicionários, cada um contendo informações sobre um issue
Parâmetros: owner, repo_nameOu seja, o cliente só pode ver a tool list_repository_issues e não a tool list_all_repository_issues.
Alteração para private
Trocamos include_tags para private para usar a 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
Teste da tag private
Executamos novamente o cliente com a alteração feita
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:
Tal como antes, não é necessário fazer uma solicitação, já que nos mostra as tools disponíveis e vemos que temos list_more_repository_issues.
🛠️ Ferramentas disponíveis (1):
==================================================
📋 listar_mais_problemas_do_repositório
Descrição: Lista os problemas abertos de um determinado repositório do GitHub.
Args:
owner: O proprietário do repositório (por exemplo, 'modelcontextprotocol')repo_name: O nome do repositório (por exemplo, 'python-sdk')
Retorna:
list[dict]: Uma lista de dicionários, cada um contendo informações sobre um problema
Parâmetros: owner, repo_nameRetorno à public
Voltamos a definir include_tags como public para usar a 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
Assim como antes filtramos as tools que podem ser usadas por tags, também podemos excluir tools por tags. Para isso, ao criar o servidor, é preciso adicionar o parâmetro exclude_tags com as tags que queremos excluir.
Servidor MCP
Criamos uma nova tool e a excluímos por meio de 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
Criamos a tool first_repository_issue, mas não vamos poder usá-la porque ela tem as tags public e first_issue, mas, ao criar o servidor, definimos exclude_tags={"first_issue"}.
Teste de exclude_tags
Executamos o 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 não está disponível a tool first_repository_issue
🛠️ Ferramentas disponíveis (1):
==================================================
📋 listar_issues_do_repositório
Descrição: Lista as issues abertas de um determinado repositório do GitHub.
Argumentos:
owner: O proprietário do repositório (por exemplo, 'modelcontextprotocol')repo_name: O nome do repositório (por exemplo, 'python-sdk')
Retorna:
list[dict]: Uma lista de dicionários, cada um contendo informações sobre um problema
Parâmetros: owner, repo_nameComposição de servidores
Assim como na programação é possível herdar classes, ou construir sobre funções já criadas, no MCP é possível criar sub-servidores e criar uma composição deles.
Servidor MCP
Vamos criar um subservidor MCP, com sua própria tool hello_world. Depois, vamos montá-lo no servidor principal. Fazendo isso, vamos poder usar a tool hello_world no cliente que se conectar ao 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
Teste da composição de servidores MCP
Executamos o 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 apareceu a nova tool sub_mcp_hello_world
🛠️ Ferramentas disponíveis (2):
==================================================
📋 sub_mcp_hello_world
Descrição: Retorna uma saudação simples.
Parâmetros:
📋 listar_issues_do_repositório
Descrição: Lista os issues abertos para um determinado repositório do GitHub.
Args:
owner: O proprietário do repositório (por exemplo, 'modelcontextprotocol')
repo_name: O nome do repositório (por exemplo, 'python-sdk')
Retorna:
list[dict]: Uma lista de dicionários, cada um contendo informações sobre um problema
Parâmetros: owner, repo_nameE quando pedimos para que nos cumprimente, ele executa.
👤 Você: Pode me cumprimentar?
🤔 Claude está pensando...
🔧 Claude quer usar: sub_mcp_hello_world
📝 Argumentos: {}✅ Ferramenta executada com sucesso
🤖 Claude: Vou ajudá-lo a enviar uma saudação usando a função `sub_mcp_hello_world`. Esta função retorna uma saudação simples. Aí está a sua saudação! A função retornou "Hello, world!"---
➡️ **Continua na Parte 2: camada de transporte, contexto e resources**.