Skip to content

Production-ready MCP server for Thailand Stock data with FastMCP, featuring HTTP/SSE transport, API authentication, and generic client examples demonstrating proper MCP design patterns.

Notifications You must be signed in to change notification settings

thakorneyp11/mcp-server-demo

Repository files navigation

Thailand Stock Market MCP Server with OAuth 2.1

Status OAuth 2.1 MCP Spec Docker

Production-ready MCP (Model Context Protocol) server for Thai stock market data with OAuth 2.1 authorization, designed for Claude.ai Custom Connector integration.

Category Features
MCP Transport /mcp (Streamable HTTP) and /sse (SSE) - both OAuth protected
Stock Data Real-time prices, company info, search from Thailand SET
OAuth 2.1 PKCE, JWT (RS256), dynamic client registration, audience validation
Production Docker, nginx, rate limiting, CORS, health checks, structured logging
Claude.ai Custom Connector

🚀 Quick Start

# Clone and configure
git clone <your-repo-url> && cd mcp-server-demo
cp .env.example .env

# Start production server (OAuth mode by default)
docker-compose -f docker-compose.prod.yml up --build -d

# Server running at http://localhost (nginx) → http://localhost:8000 (MCP server)

# Verify
curl http://localhost/health
curl http://localhost/.well-known/oauth-protected-resource

Table of Contents

📖 Project Overview

This MCP server is built with Python 3.10 and FastMCP to provide HTTP/SSE transport for remote access. It uses the yfinance library to fetch stock data from the Thailand Stock Exchange (SET) and exposes 3 tools for MCP clients

Key Capabilities:

  • Real-time stock prices, company information, and search for Thai stocks
  • OAuth 2.1 authorization for secure Claude.ai integration
  • Production-ready deployment with Docker, nginx, and Cloudflare SSL support

Deployment Options:

  • Development: Docker Compose with hot-reload
  • Testing: Docker Compose for automated testing
  • Production: Docker + nginx + Cloudflare SSL for cloud deployment

🔧 Technical Features

Stock Data Features

  • Current Price: Price in THB, change amount, percentage change, volume
  • Company Info: Full name, sector, industry, market cap, description
  • Stock Search: Search by symbol or company name (case-insensitive)

Performance Features

  • Caching: 60-second TTL cache to minimize API calls
  • Async Support: Built with asyncio for concurrent requests
  • Error Handling: Graceful error messages for invalid symbols

Data Source

  • Uses yfinance library with .BK suffix for Thai stocks
  • Example: AOTAOT.BK (Airports of Thailand)
  • Data is delayed by 15-20 minutes (yfinance limitation)

🔧 MCP Features

OAuth 2.1 Authorization

  • PKCE Enforcement: Proof Key for Code Exchange (RFC 7636)
  • JWT RS256 Tokens: Signed access tokens with configurable lifetime
  • Dynamic Client Registration: RFC 7591 compliant
  • Audience Validation: Prevents token passthrough attacks (RFC 8707)
  • Token Refresh: 30-day refresh tokens with rotation

Dual Transport Support

  • Streamable HTTP: /mcp endpoint for modern MCP clients
  • Server-Sent Events: /sse endpoint for legacy compatibility
  • Both share the same FastMCP tools and OAuth protection

OAuth Endpoints

Endpoint Description
POST /oauth/register Dynamic client registration
GET /oauth/authorize Authorization endpoint (PKCE required)
POST /oauth/token Token exchange
POST /oauth/revoke Token revocation
GET /.well-known/oauth-protected-resource Resource metadata (RFC 9728)
GET /.well-known/oauth-authorization-server AS metadata (RFC 8414)
GET /.well-known/jwks.json Public keys for JWT verification

🔧 Production Features

  • API Key Auth: Legacy mode with X-API-Key header
  • Rate Limiting: 100 requests/minute per IP (configurable)
  • CORS: Configurable cross-origin resource sharing
  • Health Checks: /health endpoint for monitoring
  • Metrics: /metrics endpoint for request tracking
  • Structured Logging: JSON logs with request IDs and duration
  • Security Headers: X-Frame-Options, X-Content-Type-Options, etc.
  • Non-root User: Runs as unprivileged mcpuser in Docker

📊 Architecture

Production Stack

┌──────────────┐
│  Cloudflare  │ (SSL Termination, DNS, DDoS Protection)
└──────┬───────┘
       │ HTTPS
┌──────▼───────┐
│    nginx     │ (Reverse Proxy, Port 80)
│              │ - Security headers
│              │ - SSE optimization
│              │ - Health checks
└──────┬───────┘
       │ HTTP
┌──────▼───────┐
│  MCP Server  │ (FastMCP + uvicorn, Port 8000)
│              │ - OAuth 2.1 / API Key auth
│              │ - Rate limiting & CORS
│              │ - Logging & metrics
│              │ - 3 MCP tools
└──────┬───────┘
       │
┌──────▼───────┐
│   yfinance   │ (Stock data API)
└──────────────┘

⚙️ Setup and Installation

Prerequisites

  • Docker and Docker Compose
  • Python 3.10+ (for local development)

Development (with hot-reload)

# Start development server
docker-compose -f docker-compose.dev.yml up --build

# Server at http://localhost:8000 with auto-reload on code changes

Testing

# Run full test suite in Docker
docker-compose -f docker-compose.test.yml up --build

# Or run locally with pytest
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
pytest -v

Production

# Configure environment
cp .env.example .env
# Edit .env: Set SERVER_URL, CORS_ORIGINS for your domain

# Start production stack (OAuth mode by default)
docker-compose -f docker-compose.prod.yml up --build -d
# optional: run `ngrok http 8000` to get a public https URL to test with Claude.ai

# Verify deployment
curl http://localhost/health
curl http://localhost/metrics

Authentication Modes (via AUTH_MODE env var):

Mode Description
oauth OAuth 2.1 Bearer tokens (default, recommended)
apikey Legacy API key via X-API-Key header
dual Both OAuth and API key supported
Full Environment Variables Reference

Authentication

Variable Default Description
AUTH_MODE oauth Authentication mode: oauth, apikey, or dual
SERVER_URL http://localhost:8000 OAuth issuer and audience URL
MCP_API_KEY - API key (required for apikey/dual modes)

OAuth Tokens

Variable Default Description
ACCESS_TOKEN_LIFETIME 900 Access token lifetime in seconds (15 min)
REFRESH_TOKEN_LIFETIME 2592000 Refresh token lifetime in seconds (30 days)
OAUTH_SCOPES mcp:read,mcp:tools Supported OAuth scopes

JWT Keys (auto-generated if empty)

Variable Description
JWT_PRIVATE_KEY RSA private key for signing
JWT_PUBLIC_KEY RSA public key for verification

Server Configuration

Variable Default Description
HOST 0.0.0.0 Bind address
PORT 8000 Server port
LOG_LEVEL INFO Logging level
RELOAD false Auto-reload (dev only)
RATE_LIMIT_PER_MINUTE 100 Rate limit per IP
CORS_ORIGINS * Allowed origins (comma-separated)

🧩 API Endpoints

Endpoint Auth Description
/mcp OAuth MCP Streamable HTTP transport
/sse OAuth MCP SSE transport
/health None Health check
/metrics None Server metrics
/oauth/* None OAuth endpoints
/.well-known/* None OAuth metadata

For detailed API documentation, see API Reference.

👉🏻 MCP Tools

get_stock_price

Get current stock price for a Thai stock symbol.

{
  "symbol": "AOT",
  "price": 73.50,
  "change": 0.50,
  "change_percent": 0.68,
  "volume": 12345678,
  "timestamp": "2025-01-15T10:30:00"
}

get_stock_info

Get detailed company information.

{
  "symbol": "AOT",
  "name": "Airports of Thailand PCL",
  "sector": "Industrials",
  "industry": "Airport Services",
  "market_cap": 500000000000,
  "description": "..."
}

search_stocks

Search for stocks by symbol or company name.

{
  "query": "bank",
  "results": [
    {"symbol": "KBANK", "name": "Kasikornbank PCL"},
    {"symbol": "BBL", "name": "Bangkok Bank PCL"},
    {"symbol": "SCB", "name": "Siam Commercial Bank PCL"}
  ]
}

🤖 Claude Desktop Integration

Local Development (stdio)

For direct Python execution with Claude Desktop:

{
  "mcpServers": {
    "thailand-stocks": {
      "command": "/path/to/venv/bin/python",
      "args": ["/path/to/mcp-server-demo/main.py"],
      "env": {}
    }
  }
}

Production (HTTP/SSE with OAuth)

For remote access to deployed server:

{
  "mcpServers": {
    "thailand-stocks": {
      "url": "https://your-domain.com/sse",
      "headers": {
        "Authorization": "Bearer <your-access-token>"
      }
    }
  }
}

Claude.ai Custom Connector

  1. Deploy server to cloud with public HTTPS URL
  2. In Claude.ai: Settings → Custom Connectors → Add https://your-domain.com/mcp
  3. Claude.ai auto-discovers OAuth metadata and handles authorization
Configure for Claude.ai

Full guide: CLAUDE_AI_INTEGRATION.md

📚 Documentation

Guide Description
Documentation Index Complete documentation directory
Docker Deployment Docker configuration and deployment
Claude.ai Integration Custom Connector setup
OAuth Implementation OAuth 2.1 technical deep dive
API Reference Complete API documentation
Troubleshooting Common issues and fixes

🔒 Security

This implementation follows the MCP Authorization Specification:

  • PKCE Enforcement: All authorization flows require PKCE
  • JWT RS256 Signing: Tokens signed with RSA keys
  • Audience Validation: Prevents token passthrough attacks
  • WWW-Authenticate Headers: RFC 9728 compliant
  • Rate Limiting: Per-IP request limits
  • CORS: Configurable origin restrictions

Production Status: ✅ READY FOR DEPLOYMENT

🛠️ Adding New MCP Tools

To add new MCP tools, follow this pattern in src/server_oauth.py:

1. Define the Tool Function

from typing import Annotated

@mcp.tool()
async def your_new_tool(
    param1: Annotated[str, "Description of parameter 1"],
    param2: Annotated[int, "Description of parameter 2 (optional)"] = 10
) -> dict:
    """Brief description of what the tool does.

    This docstring becomes the tool description visible to MCP clients.
    """
    logger.info(f"your_new_tool called with param1: {param1}")

    # Call your business logic (keep tool handlers thin)
    result = your_service.do_something(param1, param2)

    # Log outcome
    if result.get("success"):
        logger.info(f"Success: {result}")
    else:
        logger.warning(f"Failed: {result.get('error')}")

    return result

2. Key Requirements

Requirement Description
Decorator Use @mcp.tool() to register the function
Async Function must be async def
Annotated params Use Annotated[type, "description"] for auto-generated schemas
Docstring First line becomes the tool description for clients
Return dict Return {"success": True/False, ...} pattern
Logging Log inputs and outcomes for debugging

3. Business Logic Separation

Keep tool handlers thin - delegate to service modules:

src/
├── server_oauth.py      # Tool handlers (thin layer)
├── stock_service.py     # Business logic for stock tools
└── your_service.py      # Business logic for your new tools

4. Testing

Add tests in tests/test_server.py:

@pytest.mark.asyncio
async def test_your_new_tool():
    result = await your_new_tool("test_param")
    assert result["success"] is True

About

Production-ready MCP server for Thailand Stock data with FastMCP, featuring HTTP/SSE transport, API authentication, and generic client examples demonstrating proper MCP design patterns.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published