MCP with FastMCP (4/4): HTTP, authentication and the MCP client

MCP with FastMCP (4/4): HTTP, authentication and the MCP client

In the previous sections we built a complete MCP server with tools, resources, and prompts. To wrap up the guide, we return to the **HTTP** transport, add **authentication** with Bearer tokens and RSA keys, and finally create an **MCP client** that connects to the server.

⚠️ This chapter continues the code from the previous parts (Part 1 · Part 2 · Part 3).

Disclaimer: This post has been translated to English using a machine translation model. Please, let me know if you find any mistakes.

📚 **This post is part of the _MCP with FastMCP_ series**, divided into four chapters that are read in order:

> * Part 1: First server and tools

* Part 2: Transport, context, and resources

* Part 3: Advanced resources and prompts

* 👉 **Part 4: HTTP, authentication and client**

Back to HTTPlink image 12

We set http again as the transport layer for the last two things we’re going to see

MCP Serverlink image 13

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP, Context
from github import GITHUB_TOKEN, create_github_headers
import datetime
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(ctx: Context) -&gt; str:
"""
Returns information about the server.
"""
return {
"info": "This is the MCP GitHub server development for MaximoFN blog post",
"requested_id": ctx.request_id
}
# Use: ¿Puedes leer el resource github://repo/facebook/react para obtener información detallada del repositorio?
@mcp.resource("github://repo/{owner}/{repo_name}", tags={"public"})
async def repository_info(owner: str, repo_name: str, ctx: Context) -&gt; dict:
"""
Returns detailed information about a 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
Returns:
dict: Repository information including name, description, stats, etc.
"""
api_url = f"https://api.github.com/repos/{owner}/{repo_name}"
ctx.info(f"Fetching repository information from {api_url}...")
async with httpx.AsyncClient() as client:
try:
response = await client.get(api_url, headers=create_github_headers())
response.raise_for_status()
repo_data = response.json()
# Extract relevant repository information
repo_info = {
"name": repo_data.get("name"),
"full_name": repo_data.get("full_name"),
"description": repo_data.get("description"),
"owner": {
"login": repo_data.get("owner", {}).get("login"),
"type": repo_data.get("owner", {}).get("type")
},
"html_url": repo_data.get("html_url"),
"clone_url": repo_data.get("clone_url"),
"ssh_url": repo_data.get("ssh_url"),
"language": repo_data.get("language"),
"size": repo_data.get("size"), # Size in KB
"stargazers_count": repo_data.get("stargazers_count"),
"watchers_count": repo_data.get("watchers_count"),
"forks_count": repo_data.get("forks_count"),
"open_issues_count": repo_data.get("open_issues_count"),
"default_branch": repo_data.get("default_branch"),
"created_at": repo_data.get("created_at"),
"updated_at": repo_data.get("updated_at"),
"pushed_at": repo_data.get("pushed_at"),
"is_private": repo_data.get("private"),
"is_fork": repo_data.get("fork"),
"is_archived": repo_data.get("archived"),
"has_issues": repo_data.get("has_issues"),
"has_projects": repo_data.get("has_projects"),
"has_wiki": repo_data.get("has_wiki"),
"license": repo_data.get("license", {}).get("name") if repo_data.get("license") else None,
"topics": repo_data.get("topics", [])
}
ctx.info(f"Successfully retrieved information for repository {owner}/{repo_name}")
return repo_info
except httpx.HTTPStatusError as e:
error_message = e.response.json().get("message", "No additional message from API.")
if e.response.status_code == 404:
error_message = f"Repository {owner}/{repo_name} not found or is private."
elif 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,
"repository": f"{owner}/{repo_name}"
}
except Exception as e:
ctx.error(f"An unexpected error occurred: {str(e)}")
return {
"error": f"An unexpected error occurred: {str(e)}",
"repository": f"{owner}/{repo_name}"
}
@mcp.prompt(
name="generate_issues_prompt",
description="Generates a structured prompt for asking about GitHub repository issues. Use this when users want to formulate questions about repository issues, or need help creating prompts for issue analysis.",
tags={"public"}
)
def generate_issues_prompt(owner: str, repo_name: str) -&gt; str:
"""
Generates a structured prompt for asking about GitHub repository issues.
This prompt template helps users formulate clear questions about repository issues
and can be used as a starting point for issue analysis or research.
Args:
owner: Repository owner (e.g., 'huggingface', 'microsoft')
repo_name: Repository name (e.g., 'transformers', 'vscode')
Returns:
A formatted prompt asking about repository issues
"""
return f"""Please provide information about the open issues in the repository {owner}/{repo_name}.
I'm interested in:
- Current open issues and their status
- Recent issue trends and patterns
- Common issue categories or topics
- Any critical or high-priority issues
Repository: {owner}/{repo_name}"""
@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

MCP Clientlink image 14

	
< > 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_url: str):
"""
Connect to the specified FastMCP server via HTTP.
Args:
server_url: URL of the HTTP server (e.g., "http://localhost:8000/mcp")
"""
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 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 (including template resources)
resource_description = "Read a resource from the MCP server. "
if resources_list:
# Convert URIs to strings to avoid AnyUrl object issues
resource_uris = [str(r.uri) for r in resources_list]
resource_description += f"Available static resources: {', '.join(resource_uris)}. "
resource_description += "Also supports template resources like github://repo/owner/repo_name for GitHub repository information."
claude_tools.append({
"name": "read_mcp_resource",
"description": resource_description,
"input_schema": {
"type": "object",
"properties": {
"resource_uri": {
"type": "string",
"description": "URI of the resource to read. Can be static (like resource://server_info) or template-based (like github://repo/facebook/react)"
}
},
"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 HTTP 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;http_server_url&gt;")
print("📝 Example: python client.py http://localhost:8000/mcp")
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 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

Authenticationlink image 15

If we want to create an MCP server that only certain clients can connect to, we can add authentication

MCP Serverlink image 16

We create the server with authentication

	
< > Input
Python
%%writefile gitHub_MCP_server/github_server.py
import httpx
from fastmcp import FastMCP, Context
from fastmcp.server.auth import BearerAuthProvider
from fastmcp.server.auth.providers.bearer import RSAKeyPair
from fastmcp.server.dependencies import get_access_token, AccessToken
from github import GITHUB_TOKEN, create_github_headers
import datetime
USER_ID = 1234567890
# Generate RSA key pair for development and testing
print("🔐 Generating RSA key pair for authentication...")
key_pair = RSAKeyPair.generate()
# Configure Bearer authentication provider
auth_provider = BearerAuthProvider(
public_key=key_pair.public_key,
issuer="https://github-mcp.maxfn.dev",
audience="github-mcp-server",
required_scopes=["github:read"] # Global scope required for all requests
)
# Generate a test token for development
development_token = key_pair.create_token(
subject="dev-user-maxfn",
issuer="https://github-mcp.maxfn.dev",
audience="github-mcp-server",
scopes=["github:read", "github:write"],
expires_in_seconds=3600 * 24 # Token is valid for 24 hours
)
print(f"🎫 Development token generated:")
print(f" {development_token}")
print("💡 Use this token in the client to authenticate")
print("-" * 60)
# Create FastMCP server with authentication
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"},
auth=auth_provider # Add authentication to the server
)
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.")
# Get authenticated access token information
try:
access_token: AccessToken = get_access_token()
authenticated_user = access_token.client_id
user_scopes = access_token.scopes
ctx.info(f"Request authenticated for user: {authenticated_user} with scopes: {user_scopes}")
except Exception as e:
authenticated_user = "unknown"
user_scopes = []
ctx.warning(f"Could not get access token info: {e}")
# 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,
"authenticated_user": authenticated_user,
"user_scopes": user_scopes
}
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(ctx: Context) -&gt; str:
"""
Returns information about the server.
"""
return {
"info": "This is the MCP GitHub server development for MaximoFN blog post",
"requested_id": ctx.request_id
}
@mcp.resource("github://repo/{owner}/{repo_name}", tags={"public"})
async def repository_info(owner: str, repo_name: str, ctx: Context) -&gt; dict:
"""
Returns detailed information about a 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
Returns:
dict: Repository information including name, description, stats, etc.
"""
api_url = f"https://api.github.com/repos/{owner}/{repo_name}"
ctx.info(f"Fetching repository information from {api_url}...")
async with httpx.AsyncClient() as client:
try:
response = await client.get(api_url, headers=create_github_headers())
response.raise_for_status()
repo_data = response.json()
# Extract relevant repository information
repo_info = {
"name": repo_data.get("name"),
"full_name": repo_data.get("full_name"),
"description": repo_data.get("description"),
"owner": {
"login": repo_data.get("owner", {}).get("login"),
"type": repo_data.get("owner", {}).get("type")
},
"html_url": repo_data.get("html_url"),
"clone_url": repo_data.get("clone_url"),
"ssh_url": repo_data.get("ssh_url"),
"language": repo_data.get("language"),
"size": repo_data.get("size"), # Size in KB
"stargazers_count": repo_data.get("stargazers_count"),
"watchers_count": repo_data.get("watchers_count"),
"forks_count": repo_data.get("forks_count"),
"open_issues_count": repo_data.get("open_issues_count"),
"default_branch": repo_data.get("default_branch"),
"created_at": repo_data.get("created_at"),
"updated_at": repo_data.get("updated_at"),
"pushed_at": repo_data.get("pushed_at"),
"is_private": repo_data.get("private"),
"is_fork": repo_data.get("fork"),
"is_archived": repo_data.get("archived"),
"has_issues": repo_data.get("has_issues"),
"has_projects": repo_data.get("has_projects"),
"has_wiki": repo_data.get("has_wiki"),
"license": repo_data.get("license", {}).get("name") if repo_data.get("license") else None,
"topics": repo_data.get("topics", [])
}
ctx.info(f"Successfully retrieved information for repository {owner}/{repo_name}")
return repo_info
except httpx.HTTPStatusError as e:
error_message = e.response.json().get("message", "No additional message from API.")
if e.response.status_code == 404:
error_message = f"Repository {owner}/{repo_name} not found or is private."
elif 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,
"repository": f"{owner}/{repo_name}"
}
except Exception as e:
ctx.error(f"An unexpected error occurred: {str(e)}")
return {
"error": f"An unexpected error occurred: {str(e)}",
"repository": f"{owner}/{repo_name}"
}
@mcp.prompt(
name="generate_issues_prompt",
description="Generates a structured prompt for asking about GitHub repository issues. Use this when users want to formulate questions about repository issues, or need help creating prompts for issue analysis.",
tags={"public"}
)
def generate_issues_prompt(owner: str, repo_name: str) -&gt; str:
"""
Generates a structured prompt for asking about GitHub repository issues.
This prompt template helps users formulate clear questions about repository issues
and can be used as a starting point for issue analysis or research.
Args:
owner: Repository owner (e.g., 'huggingface', 'microsoft')
repo_name: Repository name (e.g., 'transformers', 'vscode')
Returns:
A formatted prompt asking about repository issues
"""
return f"""Please provide information about the open issues in the repository {owner}/{repo_name}.
I'm interested in:
- Current open issues and their status
- Recent issue trends and patterns
- Common issue categories or topics
- Any critical or high-priority issues
Repository: {owner}/{repo_name}"""
@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
# 1. Run server with uv run github_server.py. It gives you a token to use in the client.py
# 2. Run client.py with the token you got from the server.py - uv run client.py http://localhost:8000/mcp &lt;your_bearer_token&gt;
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8000,
)
Copied
>_ Output
			
Overwriting gitHub_MCP_server/github_server.py

We created an authentication provider for the server and a temporary development token

# Generate RSA key pair for development and testing
print("🔐 Generating RSA key pair for authentication...")
key_pair = RSAKeyPair.generate()

# Configure Bearer authentication provider
auth_provider = BearerAuthProvider(
public_key=key_pair.public_key,
issuer="https://github-mcp.maxfn.dev",
audience="github-mcp-server",
required_scopes=["github:read"] # Global scope required for all requests
)

# Generar un token de prueba para desarrollo
development_token = key_pair.create_token(subject="dev-user-maxfn",
issuer="https://github-mcp.maxfn.dev",
audience="github-mcp-server",
scopes=["github:read", "github:write"],
expires_in_seconds=3600 * 24 # Token is valid for 24 hours
)

print(f"🎫 Development token generated:")
print(f" {development_token}")
print("💡 Use this token in the client to authenticate")
print("-" * 60)

We use it when creating the MCP 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"},
auth=auth_provider # Add authentication to the server
)

MCP Clientlink image 17

We create the MCP client with authentication

	
< > 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
from fastmcp.client.auth import BearerAuth
# 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_url: str, auth_token: str = None):
"""
Connect to the specified FastMCP server via HTTP with optional authentication.
Args:
server_url: URL of the HTTP server (e.g., "http://localhost:8000/mcp")
auth_token: Bearer token for authentication (optional)
"""
print(f"🔗 Connecting to FastMCP HTTP server: {server_url}")
# Create authentication if token is provided
auth = None
if auth_token:
auth = BearerAuth(token=auth_token)
print("🔐 Using Bearer token authentication")
else:
print("⚠️ No authentication token provided - connecting without auth")
# Create FastMCP client for HTTP connection using SSE transport
self.client = Client(server_url, auth=auth)
# 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 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 list_available_prompts(self):
"""List available prompts in the FastMCP server."""
try:
# Get list of prompts from the server using FastMCP context
async with self.client as client:
prompts = await client.list_prompts()
if prompts:
print(f" 💭 Available prompts ({len(prompts)}):")
print("=" * 50)
for prompt in prompts:
print(f"🎯 {prompt.name}")
if prompt.description:
print(f" Description: {prompt.description}")
# Show parameters if available
if hasattr(prompt, 'arguments') and prompt.arguments:
params = []
for arg in prompt.arguments:
param_info = f"{arg.name}: {arg.description or 'No description'}"
if arg.required:
param_info += " (required)"
params.append(param_info)
print(f" Parameters: {', '.join(params)}")
print()
else:
print("⚠️ No prompts found in the server")
except Exception as e:
print(f"❌ Error listing prompts: {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 get_prompt(self, prompt_name: str, prompt_args: dict = None):
"""
Get/call a specific prompt from the server.
Args:
prompt_name: Name of the prompt to call
prompt_args: Arguments for the prompt (if any)
Returns:
str: Generated prompt content
"""
try:
async with self.client as client:
if prompt_args:
result = await client.get_prompt(prompt_name, prompt_args)
else:
result = await client.get_prompt(prompt_name)
# Extract the prompt text from the response
if hasattr(result, 'messages') and result.messages:
# FastMCP returns prompts as message objects
return ' '.join([msg.content.text for msg in result.messages if hasattr(msg.content, 'text')])
elif hasattr(result, 'content'):
return str(result.content)
else:
return str(result)
except Exception as e:
print(f"❌ Error getting prompt {prompt_name}: {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 (including template resources)
resource_description = "Read a resource from the MCP server. "
if resources_list:
# Convert URIs to strings to avoid AnyUrl object issues
resource_uris = [str(r.uri) for r in resources_list]
resource_description += f"Available static resources: {', '.join(resource_uris)}. "
resource_description += "Also supports template resources like github://repo/owner/repo_name for GitHub repository information."
claude_tools.append({
"name": "read_mcp_resource",
"description": resource_description,
"input_schema": {
"type": "object",
"properties": {
"resource_uri": {
"type": "string",
"description": "URI of the resource to read. Can be static (like resource://server_info) or template-based (like github://repo/facebook/react)"
}
},
"required": ["resource_uri"]
}
})
# Add a special tool for using prompts
prompt_description = "Generate specialized prompts from the MCP server. Use this when users want to: "
prompt_description += "- Create well-structured questions about repositories "
prompt_description += "- Get help formulating prompts for specific tasks "
prompt_description += "- Generate template questions for analysis "
if prompts_list:
prompt_names = [p.name for p in prompts_list]
prompt_description += f" Available prompts: {', '.join(prompt_names)} "
prompt_description += "- generate_issues_prompt: Creates structured questions about GitHub repository issues"
prompt_description += " IMPORTANT: Use prompts when users explicitly ask for help creating questions or prompts, or when they want to formulate better questions about repositories."
claude_tools.append({
"name": "use_mcp_prompt",
"description": prompt_description,
"input_schema": {
"type": "object",
"properties": {
"prompt_name": {
"type": "string",
"description": "Name of the prompt to use. Available: 'generate_issues_prompt'"
},
"prompt_args": {
"type": "object",
"description": "Arguments for the prompt. For generate_issues_prompt: {'owner': 'repo-owner', 'repo_name': 'repo-name'}",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (e.g., 'huggingface', 'microsoft')"
},
"repo_name": {
"type": "string",
"description": "Repository name (e.g., 'transformers', 'vscode')"
}
}
}
},
"required": ["prompt_name"]
}
})
# 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
elif tool_name == "use_mcp_prompt":
# Handle prompt usage
prompt_name = tool_args.get("prompt_name")
prompt_args = tool_args.get("prompt_args", {})
if prompt_name:
tool_result = await self.get_prompt(prompt_name, prompt_args)
print(f"💭 Prompt '{prompt_name}' generated successfully")
result_content = str(tool_result) if tool_result else "Error generating prompt"
else:
tool_result = "Error: No prompt name 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 HTTP client started. Write 'quit', 'q', 'exit', 'salir' to exit.")
print("💬 You can ask questions about GitHub repositories!")
print("📚 The client can use tools, resources, and prompts from the FastMCP server")
print()
print("💭 PROMPT Examples:")
print(" • 'Generate a prompt for asking about issues in facebook/react'")
print(" • 'Help me create a good question about microsoft/vscode issues'")
print(" • 'I need a structured prompt for analyzing tensorflow/tensorflow'")
print()
print("🔧 DIRECT Examples:")
print(" • 'Show me the issues in huggingface/transformers'")
print(" • 'Get repository info for github://repo/google/chrome'")
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) &lt; 2 or len(sys.argv) &gt; 3:
print("❌ Usage: python client.py &lt;http_server_url&gt; [auth_token]")
print("📝 Example: python client.py http://localhost:8000/mcp")
print("📝 Example with auth: python client.py http://localhost:8000/mcp &lt;your_bearer_token&gt;")
print("📝 Note: Now connects to HTTP server instead of executing script")
sys.exit(1)
server_url = sys.argv[1]
auth_token = sys.argv[2] if len(sys.argv) == 3 else None
# 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, auth_token)
# List available tools, resources, and prompts after connection
await client.list_available_tools()
await client.list_available_resources()
await client.list_available_prompts()
# 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

The authentication token is created from the token provided by the user when the client starts.

# Crear autenticación si se proporciona un token
auth = None
if auth_token:
auth = BearerAuth(token=auth_token)print("🔐 Using Bearer token authentication")
else:
print("⚠️ No se proporcionó token de autenticación - conectando sin autenticación")

The client is created with the authentication token, which will be sent to the server

# Create FastMCP client for HTTP connection using SSE transport
self.client = Client(server_url, auth=auth)

It connects to the server by sending the token

# Connect to the server
await client.connect_to_server(server_url, auth_token)

MCP test with authenticationlink image 18

Since we've switched back to http, we first need to start the server

	
< > Input
Python
!cd gitHub_MCP_server && source .venv/bin/activate && uv run github_server.py
Copied
>_ Output
			
🔐 Generating RSA key pair for authentication...
🎫 Development token generated:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2dpdGh1Yi1tY3AubWF4Zm4uZGV2Iiwic3ViIjoiZGV2LXVzZXItbWF4Zm4iLCJpYXQiOjE3NTExMDgzMDAsImV4cCI6MTc1MTE5NDcwMCwiYXVkIjoiZ2l0aHViLW1jcC1zZXJ2ZXIiLCJzY29wZSI6ImdpdGh1YjpyZWFkIGdpdGh1Yjp3cml0ZSJ9.PX6BtUhNCv9YVq1ZCh2teAU_LsdGMJx-W2jntTvVgdXv3aDyiOeMuZE9fIcqRy9zcXT1pjexqQQDiRhy8WlRL-mdKooEbIc_ffBVX9LPVaxKAzfzZTnx2lYTt6DgnebjjdNk_OsXF3ujH5s0xmGtY892j-k9P8dJLLrTrqXLhWG2NX_jqHB_kMalFd0LT83D6uXjPako_DKHjYKLc67WvZU_JglVS5eI9YCmmhMlhPHyO4FUlD9xb0DpbOgz8bO1ZExBrB_W2YKomGI_u8R56ItM8bS3eEwybPgEHfHhDNI6PNqsJ3DB1Grmc7KOmGX4LJCfPyB6mpl_bQmChKzcdg
💡 Use this token in the client to authenticate
------------------------------------------------------------
/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server/github_server.py:412: 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 12:58:20] INFO Starting MCP server 'GitHubMCP' with ]8;id=190590;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=102439;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 [27262]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

As we can see, it has generated the authentication token eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2dpdGh1Yi1tY3AubWF4Zm4uZGV2Iiwic3ViIjoiZGV2LXVzZXItbWF4Zm4iLCJpYXQiOjE3NTExMDgzMDAsImV4cCI6MTc1MTE5NDcwMCwiYXVkIjoiZ2l0aHViLW1jcC1zZXJ2ZXIiLCJzY29wZSI6ImdpdGh1YjpyZWFkIGdpdGh1Yjp3cml0ZSJ9.PX6BtUhNCv9YVq1ZCh2teAU_LsdGMJx-W2jntTvVgdXv3aDyiOeMuZE9fIcqRy9zcXT1pjexqQQDiRhy8WlRL-mdKooEbIc_ffBVX9LPVaxKAzfzZTnx2lYTt6DgnebjjdNk_OsXF3ujH5s0xmGtY892j-k9P8dJLLrTrqXLhWG2NX_jqHB_kMalFd0LT83D6uXjPako_DKHjYKLc67WvZU_JglVS5eI9YCmmhMlhPHyO4FUlD9xb0DpbOgz8bO1ZExBrB_W2YKomGI_u8R56ItM8bS3eEwybPgEHfHhDNI6PNqsJ3DB1Grmc7KOmGX4LJCfPyB6mpl_bQmChKzcdg, and it needs to be used when running the client

And now we run the client with the authentication token that the server has generated for us.

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py http://localhost:8000/mcp eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2dpdGh1Yi1tY3AubWF4Zm4uZGV2Iiwic3ViIjoiZGV2LXVzZXItbWF4Zm4iLCJpYXQiOjE3NTExMDgzMDAsImV4cCI6MTc1MTE5NDcwMCwiYXVkIjoiZ2l0aHViLW1jcC1zZXJ2ZXIiLCJzY29wZSI6ImdpdGh1YjpyZWFkIGdpdGh1Yjp3cml0ZSJ9.PX6BtUhNCv9YVq1ZCh2teAU_LsdGMJx-W2jntTvVgdXv3aDyiOeMuZE9fIcqRy9zcXT1pjexqQQDiRhy8WlRL-mdKooEbIc_ffBVX9LPVaxKAzfzZTnx2lYTt6DgnebjjdNk_OsXF3ujH5s0xmGtY892j-k9P8dJLLrTrqXLhWG2NX_jqHB_kMalFd0LT83D6uXjPako_DKHjYKLc67WvZU_JglVS5eI9YCmmhMlhPHyO4FUlD9xb0DpbOgz8bO1ZExBrB_W2YKomGI_u8R56ItM8bS3eEwybPgEHfHhDNI6PNqsJ3DB1Grmc7KOmGX4LJCfPyB6mpl_bQmChKzcdg
Copied
>_ Output
			
🔗 Connecting to FastMCP HTTP server: http://localhost:8000/mcp
🔐 Using Bearer token authentication
✅ 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')
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):
==================================================
...
📚 The client can use tools, resources, and prompts from the FastMCP server
💭 PROMPT Examples:
• 'Generate a prompt for asking about issues in facebook/react'
• 'Help me create a good question about microsoft/vscode issues'
• 'I need a structured prompt for analyzing tensorflow/tensorflow'
🔧 DIRECT Examples:
• 'Show me the issues in huggingface/transformers'
• 'Get repository info for github://repo/google/chrome'
------------------------------------------------------------
👤 You:

As we can see, the client connects to the server and gives us a list of the available tools, resources, and prompts.

Client ping to serverlink image 19

When we run the MCP with http as the transport layer, it is normal for the client and the server not to be on the same computer. Therefore, when we run the client, we cannot know whether the server is working, so we can develop a ping to check that the server is working.

MCP Clientlink image 20

We are going to add a ping to the MCP client

	
< > 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
from fastmcp.client.auth import BearerAuth
# 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_url: str, auth_token: str = None):
"""
Connect to the specified FastMCP server via HTTP with optional authentication.
Args:
server_url: URL of the HTTP server (e.g., "http://localhost:8000/mcp")
auth_token: Bearer token for authentication (optional)
"""
print(f"🔗 Connecting to FastMCP HTTP server: {server_url}")
# Create authentication if token is provided
auth = None
if auth_token:
auth = BearerAuth(token=auth_token)
print("🔐 Using Bearer token authentication")
else:
print("⚠️ No authentication token provided - connecting without auth")
# Create FastMCP client for HTTP connection using SSE transport
self.client = Client(server_url, auth=auth)
# Note: FastMCP Client automatically detects HTTP URLs and uses SSE transport
print("✅ Client created successfully")
# Ping to server to check if it's alive
async with self.client as client:
response = await client.ping()
print(f"🏓 Server ping response: {response}")
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 list_available_prompts(self):
"""List available prompts in the FastMCP server."""
try:
# Get list of prompts from the server using FastMCP context
async with self.client as client:
prompts = await client.list_prompts()
if prompts:
print(f" 💭 Available prompts ({len(prompts)}):")
print("=" * 50)
for prompt in prompts:
print(f"🎯 {prompt.name}")
if prompt.description:
print(f" Description: {prompt.description}")
# Show parameters if available
if hasattr(prompt, 'arguments') and prompt.arguments:
params = []
for arg in prompt.arguments:
param_info = f"{arg.name}: {arg.description or 'No description'}"
if arg.required:
param_info += " (required)"
params.append(param_info)
print(f" Parameters: {', '.join(params)}")
print()
else:
print("⚠️ No prompts found in the server")
except Exception as e:
print(f"❌ Error listing prompts: {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 get_prompt(self, prompt_name: str, prompt_args: dict = None):
"""
Get/call a specific prompt from the server.
Args:
prompt_name: Name of the prompt to call
prompt_args: Arguments for the prompt (if any)
Returns:
str: Generated prompt content
"""
try:
async with self.client as client:
if prompt_args:
result = await client.get_prompt(prompt_name, prompt_args)
else:
result = await client.get_prompt(prompt_name)
# Extract the prompt text from the response
if hasattr(result, 'messages') and result.messages:
# FastMCP returns prompts as message objects
return ' '.join([msg.content.text for msg in result.messages if hasattr(msg.content, 'text')])
elif hasattr(result, 'content'):
return str(result.content)
else:
return str(result)
except Exception as e:
print(f"❌ Error getting prompt {prompt_name}: {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 (including template resources)
resource_description = "Read a resource from the MCP server. "
if resources_list:
# Convert URIs to strings to avoid AnyUrl object issues
resource_uris = [str(r.uri) for r in resources_list]
resource_description += f"Available static resources: {', '.join(resource_uris)}. "
resource_description += "Also supports template resources like github://repo/owner/repo_name for GitHub repository information."
claude_tools.append({
"name": "read_mcp_resource",
"description": resource_description,
"input_schema": {
"type": "object",
"properties": {
"resource_uri": {
"type": "string",
"description": "URI of the resource to read. Can be static (like resource://server_info) or template-based (like github://repo/facebook/react)"
}
},
"required": ["resource_uri"]
}
})
# Add a special tool for using prompts
prompt_description = "Generate specialized prompts from the MCP server. Use this when users want to: "
prompt_description += "- Create well-structured questions about repositories "
prompt_description += "- Get help formulating prompts for specific tasks "
prompt_description += "- Generate template questions for analysis "
if prompts_list:
prompt_names = [p.name for p in prompts_list]
prompt_description += f" Available prompts: {', '.join(prompt_names)} "
prompt_description += "- generate_issues_prompt: Creates structured questions about GitHub repository issues"
prompt_description += " IMPORTANT: Use prompts when users explicitly ask for help creating questions or prompts, or when they want to formulate better questions about repositories."
claude_tools.append({
"name": "use_mcp_prompt",
"description": prompt_description,
"input_schema": {
"type": "object",
"properties": {
"prompt_name": {
"type": "string",
"description": "Name of the prompt to use. Available: 'generate_issues_prompt'"
},
"prompt_args": {
"type": "object",
"description": "Arguments for the prompt. For generate_issues_prompt: {'owner': 'repo-owner', 'repo_name': 'repo-name'}",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (e.g., 'huggingface', 'microsoft')"
},
"repo_name": {
"type": "string",
"description": "Repository name (e.g., 'transformers', 'vscode')"
}
}
}
},
"required": ["prompt_name"]
}
})
# 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
elif tool_name == "use_mcp_prompt":
# Handle prompt usage
prompt_name = tool_args.get("prompt_name")
prompt_args = tool_args.get("prompt_args", {})
if prompt_name:
tool_result = await self.get_prompt(prompt_name, prompt_args)
print(f"💭 Prompt '{prompt_name}' generated successfully")
result_content = str(tool_result) if tool_result else "Error generating prompt"
else:
tool_result = "Error: No prompt name 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 HTTP client started. Write 'quit', 'q', 'exit', 'salir' to exit.")
print("💬 You can ask questions about GitHub repositories!")
print("📚 The client can use tools, resources, and prompts from the FastMCP server")
print()
print("💭 PROMPT Examples:")
print(" • 'Generate a prompt for asking about issues in facebook/react'")
print(" • 'Help me create a good question about microsoft/vscode issues'")
print(" • 'I need a structured prompt for analyzing tensorflow/tensorflow'")
print()
print("🔧 DIRECT Examples:")
print(" • 'Show me the issues in huggingface/transformers'")
print(" • 'Get repository info for github://repo/google/chrome'")
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) &lt; 2 or len(sys.argv) &gt; 3:
print("❌ Usage: python client.py &lt;http_server_url&gt; [auth_token]")
print("📝 Example: python client.py http://localhost:8000/mcp")
print("📝 Example with auth: python client.py http://localhost:8000/mcp &lt;your_bearer_token&gt;")
print("📝 Note: Now connects to HTTP server instead of executing script")
sys.exit(1)
server_url = sys.argv[1]
auth_token = sys.argv[2] if len(sys.argv) == 3 else None
# 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, auth_token)
# List available tools, resources, and prompts 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

We add the ping in the connect_to_server method

# Ping to server to check if it's alive
async with self.client as client:
response = await client.ping()
print(f"🏓 Server ping response: {response}")

Ping testlink image 21

We first bring up the server

	
< > Input
Python
!cd gitHub_MCP_server && source .venv/bin/activate && uv run github_server.py
Copied
>_ Output
			
🔐 Generating RSA key pair for authentication...
🎫 Development token generated:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2dpdGh1Yi1tY3AubWF4Zm4uZGV2Iiwic3ViIjoiZGV2LXVzZXItbWF4Zm4iLCJpYXQiOjE3NTExMDkxMTIsImV4cCI6MTc1MTE5NTUxMiwiYXVkIjoiZ2l0aHViLW1jcC1zZXJ2ZXIiLCJzY29wZSI6ImdpdGh1YjpyZWFkIGdpdGh1Yjp3cml0ZSJ9.N_3QPIHW3BSn1iSSkrcaoelbwA-0D9Z3gelILb8fu1JC2JhCgtnJ0IwNqJrVhAkU0CNcykT36Q3mpCgy0hDhnFKkO9SRGVFgSw71voF5YNOkzzBY14cJERolYy9UDZA6geHxwR0rKyCGYkDH-NAKPuYWC9K7UlGfuOuzh3mp-XQ3Zy4mkyvfhiuwuaJ5_MdR0YtJj6opSRbEsVs1PtFYZETPExx3iBGck2qzLek-LxAJ6mjagPncikWeDwaYShFNPO0Ub3wm2Ok_ak_TChmN3W15MknfBXZrKcIhsNIhCrXJjZkSezp5JX49zoljdK2By9-QH1xmWCQqif_APD-hNQ
💡 Use this token in the client to authenticate
------------------------------------------------------------
/Users/macm1/Documents/web/portafolio/posts/gitHub_MCP_server/github_server.py:412: 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 13:11:52] INFO Starting MCP server 'GitHubMCP' with ]8;id=186381;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=502881;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 [31017]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

And now we run the client with the authentication token

	
< > Input
Python
!cd client_MCP && source .venv/bin/activate && uv run client.py http://localhost:8000/mcp eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2dpdGh1Yi1tY3AubWF4Zm4uZGV2Iiwic3ViIjoiZGV2LXVzZXItbWF4Zm4iLCJpYXQiOjE3NTExMDkxMTIsImV4cCI6MTc1MTE5NTUxMiwiYXVkIjoiZ2l0aHViLW1jcC1zZXJ2ZXIiLCJzY29wZSI6ImdpdGh1YjpyZWFkIGdpdGh1Yjp3cml0ZSJ9.N_3QPIHW3BSn1iSSkrcaoelbwA-0D9Z3gelILb8fu1JC2JhCgtnJ0IwNqJrVhAkU0CNcykT36Q3mpCgy0hDhnFKkO9SRGVFgSw71voF5YNOkzzBY14cJERolYy9UDZA6geHxwR0rKyCGYkDH-NAKPuYWC9K7UlGfuOuzh3mp-XQ3Zy4mkyvfhiuwuaJ5_MdR0YtJj6opSRbEsVs1PtFYZETPExx3iBGck2qzLek-LxAJ6mjagPncikWeDwaYShFNPO0Ub3wm2Ok_ak_TChmN3W15MknfBXZrKcIhsNIhCrXJjZkSezp5JX49zoljdK2By9-QH1xmWCQqif_APD-hNQ
Copied
>_ Output
			
🔗 Connecting to FastMCP HTTP server: http://localhost:8000/mcp
🔐 Using Bearer token authentication
✅ Client created successfully
🏓 Server ping response: True
🛠️ 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 HTTP client started. Write 'quit', 'q', 'exit', 'salir' to exit.
💬 You can ask questions about GitHub repositories!
📚 The client can use tools, resources, and prompts from the FastMCP server
💭 PROMPT Examples:
• 'Generate a prompt for asking about issues in facebook/react'
• 'Help me create a good question about microsoft/vscode issues'
• 'I need a structured prompt for analyzing tensorflow/tensorflow'
🔧 DIRECT Examples:
• 'Show me the issues in huggingface/transformers'
• 'Get repository info for github://repo/google/chrome'
------------------------------------------------------------
👤 You: q
👋 Bye!
🧹 Cleaning up resources...
✅ Resources released

As we can see, the server has responded to the ping

🏓 Server ping response: True

---

🎉 **You have completed the guide _MCP with FastMCP_.** Review the chapters: Part 1 · Part 2 · Part 3.

Continue reading

Last posts -->

Have you seen these projects?

Gymnasia

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

Mobile personal training app with AI assistant, exercise library, workout tracking, diet and body measurements

Horeca chatbot

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

Chatbot conversational for cooks of hotels and restaurants. A cook, kitchen manager or room service of a hotel or restaurant can talk to the chatbot to get information about recipes and menus. But it also implements agents, with which it can edit or create new recipes or menus

View all projects -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to watch any talk?

Last talks -->

Do you want to improve with these tips?

Last tips -->

Use this locally

Hugging Face spaces allow us to run models with very simple demos, but what if the demo breaks? Or if the user deletes it? That's why I've created docker containers with some interesting spaces, to be able to use them locally, whatever happens. In fact, if you click on any project view button, it may take you to a space that doesn't work.

Flow edit

Flow edit Flow edit

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
View all containers -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to train your model with these datasets?

short-jokes-dataset

HuggingFace

Dataset with jokes in English

Use: Fine-tuning text generation models for humor

231K rows 2 columns 45 MB
View on HuggingFace →

opus100

HuggingFace

Dataset with translations from English to Spanish

Use: Training English-Spanish translation models

1M rows 2 columns 210 MB
View on HuggingFace →

netflix_titles

HuggingFace

Dataset with Netflix movies and series

Use: Netflix catalog analysis and recommendation systems

8.8K rows 12 columns 3.5 MB
View on HuggingFace →
View more datasets -->