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 |
# 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- Quick Start
- Project Overview
- Technical Features
- MCP Features
- Production Features
- Architecture
- Setup and Installation
- API Endpoints
- MCP Tools
- Claude Desktop Integration
- Documentation
- Security
- Adding New MCP Tools
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
- 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)
- 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
- Uses yfinance library with
.BKsuffix for Thai stocks - Example:
AOT→AOT.BK(Airports of Thailand) - Data is delayed by 15-20 minutes (yfinance limitation)
- 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
- Streamable HTTP:
/mcpendpoint for modern MCP clients - Server-Sent Events:
/sseendpoint for legacy compatibility - Both share the same FastMCP tools and OAuth protection
| 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 |
- API Key Auth: Legacy mode with
X-API-Keyheader - Rate Limiting: 100 requests/minute per IP (configurable)
- CORS: Configurable cross-origin resource sharing
- Health Checks:
/healthendpoint for monitoring - Metrics:
/metricsendpoint 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
mcpuserin Docker
┌──────────────┐
│ 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)
└──────────────┘
- Docker and Docker Compose
- Python 3.10+ (for local development)
# Start development server
docker-compose -f docker-compose.dev.yml up --build
# Server at http://localhost:8000 with auto-reload on code changes# 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# 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/metricsAuthentication 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
| 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) |
| 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 |
| Variable | Description |
|---|---|
JWT_PRIVATE_KEY |
RSA private key for signing |
JWT_PUBLIC_KEY |
RSA public key for verification |
| 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) |
| 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.
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 detailed company information.
{
"symbol": "AOT",
"name": "Airports of Thailand PCL",
"sector": "Industrials",
"industry": "Airport Services",
"market_cap": 500000000000,
"description": "..."
}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"}
]
}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": {}
}
}
}For remote access to deployed server:
{
"mcpServers": {
"thailand-stocks": {
"url": "https://your-domain.com/sse",
"headers": {
"Authorization": "Bearer <your-access-token>"
}
}
}
}- Deploy server to cloud with public HTTPS URL
- In Claude.ai: Settings → Custom Connectors → Add
https://your-domain.com/mcp - Claude.ai auto-discovers OAuth metadata and handles authorization
Full guide: CLAUDE_AI_INTEGRATION.md
| 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 |
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
To add new MCP tools, follow this pattern in src/server_oauth.py:
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| 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 |
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
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