MCP com FastMCP (2/4): camada de transporte, contexto e resources

MCP com FastMCP (2/4): camada de transporte, contexto e resources

Na primeira parte criamos nosso primeiro servidor MCP com FastMCP e aprendemos a expor funções como tools, filtrá-las por tags e compor servidores. Neste capítulo configuramos a **camada de transporte**, controlamos quais argumentos são expostos, acessamos o **contexto** da solicitação e criamos nosso primeiro **resource**.

⚠️ Este capítulo continua o código da parte anterior. Para executá-lo, você precisa do ambiente e do servidor da Parte 1.

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 são lidos em ordem:

> * Parte 1: Primeiro servidor e tools

* 👉 **Parte 2: Transporte, contexto e resources**

* Parte 3: Recursos avançados e prompts

* Parte 4: HTTP, autenticação e cliente

Camada de transportelink image 29

Se não indicarmos ao servidor MCP a camada de transporte, por padrão é usado stdio. Mas podemos indicá-lo por meio do parâmetro transport quando o executamos

mcp.run(
transport="stdio")

No entanto, se o cliente e o servidor não estiverem no mesmo computador, podemos usar http como camada de transporte

Servidor MCPlink image 30

No servidor, só temos que indicar que queremos usar http como camada de transporte, o host e a porta.

mcp.run(
transport="streamable-http",
host="0.0.0.0",port=8000,
)
	
< > 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, run with uv run client.py http://localhost:8000/mcp
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8000,
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Cliente MCPlink image 31

No cliente, o que precisa ser alterado é que antes realizávamos a conexão passando o caminho do servidor (async def connect_to_server(self, server_script_path: str)), enquanto agora fazemos isso passando a URL do servidor (async def connect_to_server(self, server_url: str)).

	
< > 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_url: str):
"""
Connect to the specified FastMCP server via HTTP.
Args:
server_url: URL of the HTTP server (e.g., "http://localhost:8000")
"""
print(f"🔗 Connecting to FastMCP HTTP server: {server_url}")
# Create FastMCP client for HTTP connection using SSE transport
self.client = Client(server_url)
# Note: FastMCP Client automatically detects HTTP URLs and uses SSE transport
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 HTTP 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 HTTP server")
print("🌐 Connected via Server-Sent Events (SSE)")
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;http_server_url&gt;")
print("📝 Example: python client.py http://localhost:8000")
print("📝 Note: Now connects to HTTP server instead of executing script")
sys.exit(1)
server_url = sys.argv[1]
# Validate URL format
if not server_url.startswith(('http://', 'https://')):
print("❌ Error: Server URL must start with http:// or https://")
print("📝 Example: python client.py http://localhost:8000")
sys.exit(1)
# Create and run client
client = FastMCPClient()
try:
# Connect to the server
await client.connect_to_server(server_url)
# 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

Teste do MCP por httplink image 32

Para testar, primeiro precisamos executar o cliente para que a URL e a porta sejam iniciadas

	
< > Input
Python
!cd gitHub_MCP_server && source .venv/bin/activate && uv run github_server.py
Copied
>_ Output
			
/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)
DEBUG: Starting FastMCP GitHub server...
DEBUG: Server name: GitHubMCP
[06/28/25 10:33:36] INFO Starting MCP server 'GitHubMCP' with ]8;id=281189;file:///Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server/.venv/lib/python3.11/site-packages/fastmcp/server/server.py\server.py]8;;\:]8;id=128713;file:///Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server/.venv/lib/python3.11/site-packages/fastmcp/server/server.py#1297\1297]8;;\
transport 'streamable-http' on
http://0.0.0.0:8000/mcp/
INFO: Started server process [89401]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Agora executamos o cliente, fornecendo a URL do servidor MCP.

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py http://localhost:8000/mcp
Copied
>_ Output
			
🔗 Connecting to FastMCP HTTP server: http://localhost:8000/mcp
✅ Client created successfully
🛠️ 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 HTTP client started. Write 'quit', 'q', 'exit', 'salir' to exit.
💬 You can ask questions about GitHub repositories!
📚 The client can use tools from the FastMCP HTTP server
🌐 Connected via Server-Sent Events (SSE)
------------------------------------------------------------
👤 You:

Vemos que a conexão foi estabelecida sem problemas.

Retorno do servidor a STDIOlink image 33

Voltamos a estabelecer STDIO como camada de transporte do servidor

	
< > 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(
transport="stdio"
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Argumentos excluídoslink image 34

Servidor MCPlink image 35

Suponhamos que queremos ter rastreabilidade do ID do usuário que fez uma solicitação, teríamos que adicionar um parâmetro à tool que seja executada com essa informação. Mas essa informação é irrelevante para o LLM, inclusive por questões de segurança, talvez não queiramos que esse ID possa ser vazado

Portanto, para que um parâmetro não seja passado ao LLM, ao definir uma tool podemos indicar que um parâmetro seja excluído por meio de exclude_args.

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP
from github import GITHUB_TOKEN, create_github_headers
USER_ID = 1234567890
# 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"},
exclude_args=["user_id"], # user_id has to be injected by server, not provided by LLM
)
async def list_repository_issues(owner: str, repo_name: str, user_id: int = USER_ID) -&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,
"requested_by_user_id": user_id
}
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(
transport="stdio"
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Como se pode ver, na tool list_repository_issues indicámos que se exclua o parâmetro user_id.

@mcp.tool(
tags={"público", "produção"},
exclude_args=["user_id"], # user_id tem que ser injetado pelo servidor, não fornecido pelo LLM)
async def list_repository_issues(owner: str, repo_name: str, user_id: int = USER_ID) -> list[dict]:

Embora depois retornemos "requested_by_user_id": user_id

result = {
"total_encontrado": len(issues_summary),
"repositório": f"{owner}/{repo_name}",
"note": "Mostrando os primeiros 10 problemas abertos" if len(issues_summary) == 10 else f"Mostrando todos os {len(issues_summary)} problemas abertos",
"issues": issues_summary,"requested_by_user_id": user_id
}

Ou seja, estamos passando o ID para o LLM no resultado. Mas, neste caso, é para que, no momento de executar a tool, vejamos que ela foi executada com esse ID.

Contextolink image 36

Podemos passar informações de contexto do servidor para o cliente e vice-versa.

Servidor MCPlink image 37

Vamos a adicionar contexto ao nosso servidor MCP.

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP, Context
from github import GITHUB_TOKEN, create_github_headers
USER_ID = 1234567890
# 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"},
exclude_args=["user_id"], # user_id has to be injected by server, not provided by LLM
)
async def list_repository_issues(owner: str, repo_name: str, ctx: Context, user_id: int = USER_ID) -&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 context of the request
user_id: The user ID (automatically injected by the server)
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"
ctx.info(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:
ctx.info("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
})
ctx.info(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,
"requested_by_user_id": user_id
}
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.)"
ctx.error(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:
ctx.error(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(
transport="stdio"
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Substituímos todos os prints por ctx.info. Dessa forma, todas essas linhas de informação agora podem ser impressas no cliente se quisermos.

Mais tarde vamos usar isso

Criar um resourcelink image 38

Vamos criar um resource estático em nosso MCP

Servidor MCPlink image 39

Podemos converter uma função no nosso servidor em um resource por meio do decorador @mcp.resource(<ENDPOINT>).

Um resource é um endpoint que nos fornece informações. Enquanto uma tool pode realizar mudanças e/ou ações, um resource apenas nos fornece informações.

Vamos a ver isso com um exemplo.

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP, Context
from github import GITHUB_TOKEN, create_github_headers
USER_ID = 1234567890
# 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"},
exclude_args=["user_id"], # user_id has to be injected by server, not provided by LLM
)
async def list_repository_issues(owner: str, repo_name: str, ctx: Context, user_id: int = USER_ID) -&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 context of the request
user_id: The user ID (automatically injected by the server)
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"
ctx.info(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:
ctx.info("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
})
ctx.info(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,
"requested_by_user_id": user_id
}
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.)"
ctx.error(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:
ctx.error(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)}"}]
@mcp.resource("resource://server_info", tags={"public"})
def server_info() -&gt; str:
"""
Returns information about the server.
"""
return "This is the MCP GitHub server development for MaximoFN blog post"
@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(
transport="stdio"
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

Como vemos, criamos o resource server_info que nos devolve uma cadeia de texto com as informações do servidor

É importante observar que declaramos o endpoint resource://server_info, que é obrigatório sempre que criamos resources

Além disso, adicionamos a tag public, já que nosso servidor MCP só inclui as tools ou resources que tenham a tag public.

mcp = FastMCP(
name="GitHubMCP",
instructions="Este servidor fornece ferramentas, recursos e prompts para interagir com a GitHub API.",
include_tags={"public"},
exclude_tags={"first_issue"}
)

Cliente MCPlink image 40

Agora temos que fazer com que nosso cliente possa ver os resources do nosso servidor MCP.

	
< > 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 and resources 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 list_available_resources(self):
"""List available resources in the FastMCP server."""
try:
# Get list of resources from the server using FastMCP context
async with self.client as client:
resources = await client.list_resources()
if resources:
print(f" 📚 Available resources ({len(resources)}):")
print("=" * 50)
for resource in resources:
print(f"📄 {resource.uri}")
if resource.name:
print(f" Name: {resource.name}")
if resource.description:
print(f" Description: {resource.description}")
if resource.mimeType:
print(f" MIME Type: {resource.mimeType}")
print()
else:
print("⚠️ No resources found in the server")
except Exception as e:
print(f"❌ Error listing resources: {str(e)}")
async def read_resource(self, resource_uri: str):
"""
Read a specific resource from the server.
Args:
resource_uri: URI of the resource to read
Returns:
str: Resource content
"""
try:
async with self.client as client:
result = await client.read_resource(resource_uri)
return result
except Exception as e:
print(f"❌ Error reading resource {resource_uri}: {str(e)}")
return None
async def process_query(self, query: str) -&gt; str:
"""
Process a user query, interacting with Claude and FastMCP tools and resources.
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 and resources
tools_list = await client.list_tools()
resources_list = await client.list_resources()
# 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)
# Add a special tool for reading resources
if resources_list:
# Convert URIs to strings to avoid AnyUrl object issues
resource_uris = [str(r.uri) for r in resources_list]
claude_tools.append({
"name": "read_mcp_resource",
"description": "Read a resource from the MCP server. Available resources: " +
", ".join(resource_uris),
"input_schema": {
"type": "object",
"properties": {
"resource_uri": {
"type": "string",
"description": "URI of the resource to read"
}
},
"required": ["resource_uri"]
}
})
# 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:
if tool_name == "read_mcp_resource":
# Handle resource reading
resource_uri = tool_args.get("resource_uri")
if resource_uri:
tool_result = await client.read_resource(resource_uri)
print(f"📖 Resource read successfully: {resource_uri}")
# Better handling of resource result
if hasattr(tool_result, 'content'):
# If it's a resource response object, extract content
if hasattr(tool_result.content, 'text'):
result_content = tool_result.content.text
else:
result_content = str(tool_result.content)
else:
# If it's already a string or simple object
result_content = str(tool_result)
else:
tool_result = "Error: No resource URI provided"
result_content = tool_result
else:
# Execute regular tool on the FastMCP server
tool_result = await client.call_tool(tool_name, tool_args)
print(f"✅ Tool executed successfully")
result_content = str(tool_result)
# Add tool result to the conversation
messages.append({
"role": "assistant",
"content": response.content
})
# Format result for Claude
if 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 and resources 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 and resources after connection
await client.list_available_tools()
await client.list_available_resources()
# 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

Criámos os métodos list_available_resources e read_resource para poder ler os recursos que temos no servidor MCP.

Teste de resourcelink image 41

Executamos o cliente para poder testar o resource que criámos

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && python 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:255: 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 11:09:01] 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')
ctx: The context of the request
user_id: The user ID (automatically injected by the server)
Returns:
list[dict]: A list of dictionaries, each containing information about an issue
Parameters: owner, repo_name
📚 Available resources (1):
==================================================
📄 resource://server_info
Name: server_info
Description: Returns information about the server.
MIME Type: text/plain
🤖 FastMCP client started. Write 'quit', 'q', 'exit', 'salir' to exit.
💬 You can ask questions about GitHub repositories!
📚 The client can use tools and resources from the FastMCP server
------------------------------------------------------------
👤 You: Tell me the server info
🤔 Claude is thinking...
🔧 Claude wants to use: read_mcp_resource
📝 Arguments: {'resource_uri': 'resource://server_info'}
📖 Resource read successfully: resource://server_info
🤖 Claude: I'll help you read the server information using the `read_mcp_resource` function with the specific resource URI for server info.The server information indicates that this is the MCP GitHub server development environment for MaximoFN blog post.
👤 You: q
👋 Bye!
🧹 Cleaning up resources...
✅ Resources released

Vemos que nos dá uma lista de resources

📚 Recursos disponíveis (1):
==================================================
📄 resource://server_info
Nome: server_info
Descrição: Retorna informações sobre o servidor.
Tipo MIME: text/plain

E que, ao pedirmos as informações do servidor, use o resource server_info que acabamos de criar.

👤 Você: Me diga as informações do servidor

🤔 Claude está pensando...
🔧 Claude quer usar: read_mcp_resource
📝 Argumentos: {'resource_uri': 'resource://server_info'}📖 Recurso lido com sucesso: resource://server_info

🤖 Claude: Vou ajudar você a ler as informações do servidor usando a função `read_mcp_resource` com o URI de recurso específico para informações do servidor. As informações do servidor indicam que este é o ambiente de desenvolvimento do servidor MCP GitHub para o post do blog MaximoFN.

---

➡️ **Continua na Parte 3: resources avançados e prompts**.

Continuar lendo

Últimos posts -->

Você viu esses projetos?

Gymnasia

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

Aplicativo móvel de treino pessoal com assistente de IA, biblioteca de exercícios, acompanhamento de rotinas, dieta e medidas corporais

Horeca chatbot

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

Chatbot conversacional para cozinheiros de hotéis e restaurantes. Um cozinheiro, gerente de cozinha ou serviço de quarto de um hotel ou restaurante pode falar com o chatbot para obter informações sobre receitas e menus. Mas também implementa agentes, com os quais pode editar ou criar novas receitas ou menus

Naviground

Naviground Naviground
Ver todos os projetos -->
>_ Disponível para projetos

Tem um projeto com IA?

Vamos conversar.

maximofn@gmail.com

Especialista em Machine Learning e Inteligência Artificial. Desenvolvo soluções com IA generativa, agentes inteligentes e modelos personalizados.

Quer assistir alguma palestra?

Últimas palestras -->

Quer melhorar com essas dicas?

Últimos tips -->

Use isso localmente

Os espaços do Hugging Face nos permitem executar modelos com demos muito simples, mas e se a demo quebrar? Ou se o usuário a deletar? Por isso, criei contêineres docker com alguns espaços interessantes, para poder usá-los localmente, aconteça o que acontecer. Na verdade, se você clicar em qualquer botão de visualização de projeto, ele pode levá-lo a um espaço que não funciona.

Flow edit

Flow edit Flow edit

Edite imagens com este modelo de Flow. Baseado em SD3 ou FLUX, você pode editar qualquer imagem e gerar novas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos os contêineres -->
>_ Disponível para projetos

Tem um projeto com IA?

Vamos conversar.

maximofn@gmail.com

Especialista em Machine Learning e Inteligência Artificial. Desenvolvo soluções com IA generativa, agentes inteligentes e modelos personalizados.

Você quer treinar seu modelo com esses datasets?

short-jokes-dataset

HuggingFace

Dataset com piadas em inglês

Uso: Fine-tuning de modelos de geração de texto humorístico

231K linhas 2 colunas 45 MB
Ver no HuggingFace →

opus100

HuggingFace

Dataset com traduções de inglês para espanhol

Uso: Treinamento de modelos de tradução inglês-espanhol

1M linhas 2 colunas 210 MB
Ver no HuggingFace →

netflix_titles

HuggingFace

Dataset com filmes e séries da Netflix

Uso: Análise de catálogo Netflix e sistemas de recomendação

8.8K linhas 12 colunas 3.5 MB
Ver no HuggingFace →
Ver mais datasets -->