thakorneyp11/mcp-server-demo
If you are the rightful owner of mcp-server-demo and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
This is a production-ready MCP server for accessing Thai stock market data, featuring OAuth 2.1 authorization and designed for integration with Claude.ai Custom Connector.
Thailand Stock Market MCP Server with OAuth 2.1
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 |
🚀 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
- 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
📖 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
.BKsuffix for Thai stocks - Example:
AOT→AOT.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:
/mcpendpoint for modern MCP clients - Server-Sent Events:
/sseendpoint 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-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
📊 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 .
👉🏻 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
- 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:
📚 Documentation
| Guide | Description |
|---|---|
| Complete documentation directory | |
| Docker configuration and deployment | |
| Custom Connector setup | |
| OAuth 2.1 technical deep dive | |
| Complete API documentation | |
| 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