A production-grade Model Context Protocol server implementing enterprise security, observability, and AI-agent safety controls.
Stack: Node.js 20 + TypeScript · OAuth 2.1 (RS256 JWT) · OpenTelemetry · Prometheus · Jaeger
| Capability | Implementation |
|---|---|
| OAuth 2.1 scoped auth | Per-tool scope enforcement, RS256 JWT validation, JWKS caching |
| Human-in-the-loop elicitation | Two-phase write pattern: initiate → confirm, with owner binding and idempotency |
| Full observability | OTel spans per tool call, Prometheus metrics, append-only audit log with hash-chain integrity |
| Security self-scan | scan_tools MCP tool (RedForge patterns): 14 checks across tool definitions, injection risk, input validation, scope isolation |
| Safe financial data | Alpha Vantage (stocks) + Open Exchange Rates (forex) — real APIs, no sensitive data |
# 1. Install
npm install
# 2. Generate RSA key pair + demo tokens
npm run generate-tokens
# 3. Run the full demo
bash scripts/run-demo.shThe demo script:
- Starts Redis + Jaeger + Prometheus + Grafana via Docker Compose
- Exercises all security scenarios (scope denial, elicitation, OWNERSHIP_MISMATCH, self-scan)
- Prints the audit log path and observability URLs
# Mock data, demo JWT verification
MOCK_EXTERNAL_APIS=true DEMO_MODE=true npm startThe server communicates over stdio (MCP standard). Build first, then add to ~/Library/Application Support/Claude/claude_desktop_config.json:
npm run build # compiles TypeScript → dist/src/mcp/server.js{
"mcpServers": {
"financial-mcp": {
"command": "node",
"args": ["/absolute/path/to/FinMCP/dist/src/mcp/server.js"],
"env": {
"DEMO_MODE": "true",
"DEMO_TOKEN_ROLE": "admin",
"MOCK_EXTERNAL_APIS": "false",
"ALPHA_VANTAGE_API_KEY": "your_key",
"OPEN_EXCHANGE_APP_ID": "your_app_id",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318",
"OTEL_SERVICE_NAME": "financial-mcp-server",
"AUDIT_LOG_PATH": "/absolute/path/to/FinMCP/logs/audit.log",
"REDIS_PENDING_OPS_TTL": "600",
"RATE_LIMIT_READ_BUCKET": "60",
"RATE_LIMIT_READ_REFILL_SECONDS": "60",
"RATE_LIMIT_WRITE_BUCKET": "10",
"RATE_LIMIT_WRITE_REFILL_SECONDS": "60"
}
}
}
}Note: Use absolute paths throughout — Claude Desktop spawns the process without a shell so relative paths and
.envfile loading do not work. After any code change runnpm run buildand restart Claude Desktop.
Tokens are passed via _meta.authorization in MCP requests:
{
"method": "tools/call",
"params": {
"name": "get_exchange_rates",
"arguments": { "base_currency": "USD" },
"_meta": { "authorization": "Bearer <token>" }
}
}Demo tokens (after npm run generate-tokens):
| Token | Scope | Can call |
|---|---|---|
read_token |
read:rates |
All read tools |
write_token |
read:rates write:alerts |
All tools + elicitation write flow |
admin_token |
admin |
All tools including scan_tools |
| Tool | Description |
|---|---|
get_exchange_rates |
FX rates for a base currency (up to 10 pairs) |
get_stock_quote |
Live stock quote (price, volume, daily change) |
convert_currency |
Currency conversion with live rate |
get_portfolio_summary |
P&L summary for a list of positions |
| Tool | Description |
|---|---|
set_price_alert |
Create a price threshold alert |
delete_price_alert |
Delete an existing alert |
update_alert_threshold |
Update the threshold on an alert |
| Tool | Scope | Description |
|---|---|---|
confirm_operation |
write:alerts |
Confirm a pending elicitation (Phase 2) |
scan_tools |
admin |
Run RedForge security scan against all tool definitions |
Agent → set_price_alert(params)
← { type: "elicitation_required", operation_id: "elicit_abc", prompt: "...", expires_at: "..." }
Agent → confirm_operation({ operation_id: "elicit_abc" })
← { type: "operation_completed", result: { alert_id: "...", status: "active" } }
Security properties enforced:
- Owner binding — only the user who initiated can confirm (
OWNERSHIP_MISMATCHotherwise) - Idempotency — confirming twice returns
already_executedwith cached result - TTL — operations expire after 10 minutes (configurable via
REDIS_PENDING_OPS_TTL)
| Endpoint | Default | What |
|---|---|---|
| Prometheus metrics | http://localhost:9464/metrics |
mcp_tool_calls_total, mcp_tool_call_duration_ms, mcp_elicitations_total, mcp_scope_denials_total |
| Jaeger UI | http://localhost:16686 |
Full OTel traces per tool call with identity + scope attributes |
| Grafana | http://localhost:3001 |
Dashboards (pre-configured Prometheus + Jaeger datasources) |
| Audit log | ./logs/audit.log |
Append-only JSONL with hash-chain integrity |
# .env
ALPHA_VANTAGE_API_KEY=your_key # alphavantage.co — free tier: 25 calls/day
OPEN_EXCHANGE_APP_ID=your_app_id # openexchangerates.org
MOCK_EXTERNAL_APIS=falseDaily quota for Alpha Vantage is tracked in-process. When exhausted, forex tools automatically fall back to Open Exchange Rates.
OAUTH_ISSUER=https://your-auth-server.com
OAUTH_AUDIENCE=financial-mcp-server
OAUTH_JWKS_URI=https://your-auth-server.com/.well-known/jwks.json
DEMO_MODE=falseThe server caches JWKS in memory (TTL: JWKS_CACHE_TTL_SECONDS, default 1h) and serves stale keys if the issuer is temporarily unreachable.
npm test # run all tests (vitest)
npm run build # compile TypeScript → dist/ (required before connecting to Claude Desktop)
npm run dev # run with tsx watch for local development (no build step needed)20 tests covering: elicitation store (TTL, idempotency, expiry), scope enforcement (all tool/scope combos), RedForge scanner (7 check scenarios), and mock data clients.
TraceForge (pip install traceforge-llm[anthropic]) is a Python observability library. This server produces OTel-native spans in the same format TraceForge expects, so Python-side test harnesses can validate traces using tf_assert and tf_snapshot fixtures against the OTel backend.
Security scan checks are implemented inline in src/redforge/ (14 checks across 4 categories). When RedForge is published to npm, scanner.ts becomes a one-line import swap — the SecurityScanResult interface is forward-compatible.