mcp-oauth-joke-server

justinlevi/mcp-oauth-joke-server

3.3

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.

Tools
2
Resources
0
Prompts
0

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

  1. Clone the repository:
git clone <repository-url>
cd mcp
  1. Install dependencies:
uv sync
  1. 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 information
  • GET /health - Health check
  • POST /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

  1. Start Keycloak and PostgreSQL:
docker-compose up -d
  1. Configure Keycloak automatically:
python scripts/configure-keycloak.py
  1. 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

  1. 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'
  1. 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

VariableDescriptionDefault
KEYCLOAK_URLKeycloak server URLhttp://localhost:8080
KEYCLOAK_REALMKeycloak realm namemcp
KEYCLOAK_CLIENT_IDClient ID for introspectionmcp-joke-server
KEYCLOAK_CLIENT_SECRETClient secret for introspectionRequired
RESOURCE_SERVER_URLThis server's URLhttp://localhost:8000
ALLOW_AUTH_BYPASSSkip auth for developmentfalse

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:

  1. Update list_tools() to include the new tool definition
  2. Update call_tool() to handle the new tool
  3. 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:
  1. Test Public Tool (No Auth Required):

    • Call get_dad_joke - should work without authentication
  2. 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

What to test:

  1. Tool Discovery: Verify both tools appear in the tools list
  2. Public Tool Access: Call get_dad_joke without authentication
  3. Protected Tool Access: Call get_mom_joke - verify 401 response
  4. OAuth Flow: Complete authentication flow via Inspector UI
  5. Authenticated Access: Call get_mom_joke with valid token
  6. Token Expiration: Wait for token to expire and verify re-authentication
  7. Error Handling: Test with invalid tool names
  8. 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 initialization
  • tools/list - List available tools
  • tools/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

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with tests
  4. Run the test suite
  5. Submit a pull request

License

MIT License - See LICENSE file for details

Resources

Acknowledgments

Built with: