justinlevi/mcp-oauth-joke-server
If you are the rightful owner of mcp-oauth-joke-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.
The Joke MCP Server is a Model Context Protocol server designed to provide dad and mom joke generation tools.
Joke MCP Server
A Model Context Protocol (MCP) server that provides dad and mom joke generation tools.
Features
- ๐ญ Two Joke Tools: Get random dad jokes or mom jokes on demand
- ๐ Multiple Transports: Supports both stdio and HTTP/SSE transports
- ๐ OAuth 2.1 Authorization: Selective tool protection with RFC 9728 compliance
- โ Type Safe: Fully typed Python code with comprehensive type hints
- ๐งช Well Tested: Comprehensive test coverage for core functionality
- ๐ฆ Modern Stack: Built with uv, FastAPI, and the official MCP SDK
Installation
Prerequisites
- Python 3.10 or higher
- uv package manager
Setup
- Clone the repository:
git clone <repository-url>
cd mcp
- Install dependencies:
uv sync
- Run tests to verify installation:
uv run pytest
Usage
Stdio Transport (Default)
The stdio transport is ideal for local integrations and MCP-compatible applications.
Start the server:
uv run joke-server
Test with MCP Inspector:
npx @modelcontextprotocol/inspector uv --directory /Users/jwinter/projects/mcp run joke-server
Configure in MCP Client:
Add to your MCP client configuration (e.g., ~/.config/claude/config.json
):
{
"mcpServers": {
"joke-server": {
"command": "uv",
"args": [
"--directory",
"/Users/jwinter/projects/mcp",
"run",
"joke-server"
]
}
}
}
HTTP Transport
The HTTP transport provides a REST API with Server-Sent Events (SSE) for streaming.
Start the HTTP server:
uv run python -m joke_mcp_server.http_server
The server will start on http://127.0.0.1:8000
by default.
Available Endpoints:
GET /
- Server informationGET /health
- Health checkPOST /mcp
- Main MCP protocol endpoint (Streamable HTTP)
Test the HTTP server:
# Get server info
curl http://127.0.0.1:8000/
# Health check
curl http://127.0.0.1:8000/health
# List tools
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
# Get a dad joke
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_dad_joke",
"arguments": {}
}
}'
OAuth 2.1 Authorization
The MCP Joke Server implements OAuth 2.1 authorization following RFC 9728 (Protected Resource Metadata). This allows selective protection of tools - dad jokes are public, while mom jokes require authentication.
Quick Start with OAuth
- Start Keycloak and PostgreSQL:
docker-compose up -d
- Configure Keycloak automatically:
python scripts/configure-keycloak.py
- Start the server with OAuth enabled:
export KEYCLOAK_URL=http://localhost:8080
export KEYCLOAK_REALM=mcp
export KEYCLOAK_CLIENT_ID=mcp-joke-server
export KEYCLOAK_CLIENT_SECRET=<get-from-keycloak-admin>
export RESOURCE_SERVER_URL=http://localhost:8000
uv run python -m joke_mcp_server.http_server
Authorization Flow
sequenceDiagram
participant Client
participant MCP Server
participant Keycloak
Client->>MCP Server: Call get_mom_joke (no token)
MCP Server-->>Client: 401 + WWW-Authenticate header
Client->>MCP Server: GET /.well-known/oauth-protected-resource
MCP Server-->>Client: Authorization server metadata
Client->>Keycloak: Request access token
Keycloak-->>Client: Access token with tools:mom_jokes scope
Client->>MCP Server: Call get_mom_joke (with token)
MCP Server->>Keycloak: Introspect token
Keycloak-->>MCP Server: Token valid + claims
MCP Server-->>Client: Mom joke response
Protected Resource Metadata
The server exposes OAuth metadata at /.well-known/oauth-protected-resource
:
{
"resource": "http://localhost:8000",
"authorization_servers": ["http://localhost:8080/realms/mcp"],
"scopes_supported": ["tools:mom_jokes"],
"bearer_methods_supported": ["header"],
"resource_name": "MCP Joke Server"
}
Testing OAuth Flow
- Get an access token:
curl -X POST http://localhost:8080/realms/mcp/protocol/openid-connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&client_id=mcp-inspector&username=testuser&password=testpass&scope=tools:mom_jokes'
- Call protected tool with token:
curl -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_mom_joke","arguments":{}}}'
Environment Variables
Variable | Description | Default |
---|---|---|
KEYCLOAK_URL | Keycloak server URL | http://localhost:8080 |
KEYCLOAK_REALM | Keycloak realm name | mcp |
KEYCLOAK_CLIENT_ID | Client ID for introspection | mcp-joke-server |
KEYCLOAK_CLIENT_SECRET | Client secret for introspection | Required |
RESOURCE_SERVER_URL | This server's URL | http://localhost:8000 |
ALLOW_AUTH_BYPASS | Skip auth for development | false |
Tool Protection Status
- ๐ get_dad_joke: Public - no authentication required
- ๐ get_mom_joke: Protected - requires
tools:mom_jokes
scope
Available Tools
1. get_dad_joke
Get a random dad joke. Dad jokes are known for being cheesy, corny, and often involving puns or wordplay.
Input Schema:
{
"type": "object",
"properties": {},
"required": []
}
Example Response:
"Why don't scientists trust atoms? Because they make up everything!"
2. get_mom_joke
Get a random mom joke. These are classic sayings and phrases that mothers often use.
Input Schema:
{
"type": "object",
"properties": {},
"required": []
}
Example Response:
"Because I said so, that's why!"
Development
Project Structure
mcp/
โโโ src/
โ โโโ joke_mcp_server/
โ โโโ __init__.py # Package initialization
โ โโโ jokes.py # Joke generation logic
โ โโโ server.py # Stdio MCP server
โ โโโ http_server.py # HTTP/SSE MCP server
โ โโโ auth.py # OAuth 2.1 authorization
โโโ tests/
โ โโโ __init__.py
โ โโโ test_jokes.py # Joke generator tests
โ โโโ test_http_server.py # HTTP server tests
โโโ pyproject.toml # Project configuration
โโโ pytest.ini # Pytest configuration
โโโ mcp-config.json # MCP inspector config
โโโ README.md # This file
Running Tests
# Run all tests
uv run pytest
# Run with coverage report
uv run pytest --cov=joke_mcp_server --cov-report=html
# Run specific test file
uv run pytest tests/test_jokes.py
# Run with verbose output
uv run pytest -v
Code Quality
The codebase follows modern Python best practices:
- Type Safety: Full type hints throughout
- Documentation: Comprehensive docstrings
- Testing: TDD approach with high coverage
- Error Handling: Graceful error handling and logging
- Modern Async: Async/await for I/O operations
Adding New Jokes
To add new jokes, edit the collections in src/joke_mcp_server/jokes.py
:
DAD_JOKES = [
"Your new joke here!",
# ... existing jokes
]
MOM_JOKES = [
"Your new mom saying here!",
# ... existing jokes
]
Adding New Tools
To add a new tool:
- Update
list_tools()
to include the new tool definition - Update
call_tool()
to handle the new tool - Add tests for the new functionality
Architecture
Stdio Transport
The stdio transport (server.py
) uses the MCP SDK's stdio server implementation:
Client โ stdin โ MCP Server โ stdout โ Client
HTTP Transport
The HTTP transport (http_server.py
) provides:
- POST /mcp: Streamable HTTP transport for JSON-RPC 2.0 message handling
- Stateless operation: Each request is independent
Testing with MCP Inspector
The MCP Inspector is the official testing tool.
Start Inspector with stdio transport:
npx @modelcontextprotocol/inspector uv --directory . run joke-server
Testing with OAuth Authentication
To test the OAuth flow with MCP Inspector, you need to configure the authentication settings in the Inspector's Auth tab.
1. Start the required services:
# Start Keycloak and PostgreSQL
docker-compose up -d
# Configure Keycloak (creates realm, clients, users)
python scripts/configure-keycloak.py
# Start the HTTP server with OAuth enabled
export KEYCLOAK_URL=http://localhost:8080
export KEYCLOAK_REALM=mcp
export KEYCLOAK_CLIENT_ID=mcp-joke-server
export KEYCLOAK_CLIENT_SECRET=$(cat /tmp/client_secret.txt)
export RESOURCE_SERVER_URL=http://localhost:8000
export ALLOW_AUTH_BYPASS=false
uv run python -m joke_mcp_server.http_server
2. Configure MCP Inspector Authentication:
In the MCP Inspector UI, go to the Auth tab and configure the following settings:
OAuth 2.0 Flow Settings:
- Client ID:
mcp-inspector
- Redirect URL:
http://localhost:6274/oauth/callback
- Scope:
openid profile email tools:mom_jokes
Authorization Endpoints:
The Inspector will auto-discover these from the /.well-known/oauth-protected-resource
endpoint, but for reference:
- Authorization Server:
http://localhost:8080/realms/mcp
- Token Endpoint:
http://localhost:8080/realms/mcp/protocol/openid-connect/token
- Authorization Endpoint:
http://localhost:8080/realms/mcp/protocol/openid-connect/auth
Test User Credentials (created by configure-keycloak.py):
- Username:
testuser
- Password:
testpass
3. Test the Authorization Flow:
-
Test Public Tool (No Auth Required):
- Call
get_dad_joke
- should work without authentication
- Call
-
Test Protected Tool (Auth Required):
- Call
get_mom_joke
without authentication - should return 401 - Click "Authenticate" in the Inspector
- Log in with test credentials
- Call
get_mom_joke
again - should now succeed
- Call
What to test:
- Tool Discovery: Verify both tools appear in the tools list
- Public Tool Access: Call
get_dad_joke
without authentication - Protected Tool Access: Call
get_mom_joke
- verify 401 response - OAuth Flow: Complete authentication flow via Inspector UI
- Authenticated Access: Call
get_mom_joke
with valid token - Token Expiration: Wait for token to expire and verify re-authentication
- Error Handling: Test with invalid tool names
- Protocol Compliance: Verify MCP protocol messages
MCP Protocol Implementation
This server implements the Model Context Protocol specification:
- Protocol Version: 2024-11-05
- Capabilities: Tools
- Message Format: JSON-RPC 2.0
- Transports: stdio, HTTP/SSE
Supported MCP Methods:
initialize
- Server initializationtools/list
- List available toolstools/call
- Execute a tool
Performance
- Lightweight: Minimal dependencies
- Fast startup: < 1 second
- Low memory: < 50MB RSS
- Async I/O: Non-blocking operations
Security
- Binds to localhost by default (HTTP mode)
- No external API calls
- No data persistence
- No authentication required (local use)
Troubleshooting
Server won't start
Ensure uv is installed and dependencies are synced:
uv --version
uv sync
MCP Inspector can't connect
Verify the command path in mcp-config.json matches your installation:
pwd # Get current directory path
Tests failing
Ensure all dependencies are installed:
uv sync --all-extras
Contributing
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Run the test suite
- Submit a pull request
License
MIT License - See LICENSE file for details
Resources
Acknowledgments
Built with: