Skip to main content
Glama
Tejas-gaikwad

trading-mcp

Trading MCP Server (JWT-authenticated)

A small but production-shaped Model Context Protocol server that Claude can connect to and drive in natural language. It exposes a five-tool paper-trading domain, and every tool call is gated behind JWT authentication that is verified on each invocation — not once at connection time.

The trading domain was chosen deliberately: because holdings, cash, and orders are strictly per-user, the auth layer is doing real work on every call, which makes it a more meaningful demonstration of authentication than a shared or read-only dataset would be.


Tools

Tool

Auth required

Purpose

login(username, password)

no

Validate credentials, return a signed JWT.

get_quote(token, symbol)

yes

Current simulated price + day change for a symbol.

place_order(token, symbol, side, quantity, order_type)

yes

Execute a simulated market buy/sell against the user's own portfolio.

get_portfolio(token)

yes

Cash, positions, and live unrealized P&L.

get_order_history(token)

yes

The user's past executed orders.

Market prices are simulated from a seeded local table — no external API key, fully reproducible. See Assumptions & trade-offs.

Demo accounts

Username

Password

demo

demo123

alice

alice123

Each user has their own cash, holdings, and history — useful for confirming that one user's token can never see another's data.


Setup

Requires Python 3.10+. A recent-but-not-bleeding-edge version (3.11–3.13) is recommended; see the note under Connecting to Claude Desktop if you are on 3.14.

git clone <your-repo-url>
cd trading-mcp

python3 -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt

Set the signing secret

The JWT signing secret is read from the environment and is never committed. The server refuses to start without it.

cp .env.example .env
# then edit .env, or generate a strong secret in one line:
echo "JWT_SECRET=$(python -c 'import secrets;print(secrets.token_hex(32))')" > .env

Verify it works (before touching Claude)

Two independent checks:

# 1. Unit tests: auth core + tool behavior
python -m pytest -q

# 2. End-to-end: launches the server over the real stdio MCP protocol,
#    lists tools, and exercises login + a protected call.
export JWT_SECRET="$(python -c 'import secrets;print(secrets.token_hex(32))')"
python smoke_test.py

smoke_test.py printing ALL SMOKE CHECKS PASSED means the server is wired correctly and will connect to any MCP client.


Connecting to Claude Desktop

  1. Find the absolute path to your virtualenv's Python and to server.py:

    echo "$(pwd)/.venv/bin/python"
    echo "$(pwd)/server.py"
  2. In Claude Desktop: Settings → Developer → Edit Config, and add:

    {
      "mcpServers": {
        "trading-mcp": {
          "command": "/ABSOLUTE/PATH/TO/trading-mcp/.venv/bin/python",
          "args": ["/ABSOLUTE/PATH/TO/trading-mcp/server.py"],
          "env": {
            "JWT_SECRET": "paste-a-long-random-secret-here"
          }
        }
      }
    }
  3. Fully quit Claude Desktop (Cmd+Q / quit, not just close the window) and reopen it. The server should show as running under Settings → Developer.

Important: use the venv's Python by absolute path

command must point at the virtualenv's Python (.venv/bin/python), not a bare python / python3. The client resolves a bare command against your system PATH and pins the result — on a machine whose default is a Python without the mcp package installed, the server starts and immediately exits with ModuleNotFoundError: No module named 'mcp'. An absolute venv path has nothing to resolve, so it is used as-is.

Python 3.14 note: some dependencies may not yet ship prebuilt wheels for 3.14, so pip install can fail while building them. If so, create the venv with python3.12 -m venv .venv (or 3.13) and reinstall — everything else is identical.


Using it

Because the token is passed as a normal tool argument, you drive everything in plain English and Claude handles the JSON:

  • "Log into my trading account, username demo, password demo123."login

  • "What's my portfolio?"get_portfolio

  • "What's Apple trading at?"get_quote

  • "Buy 15 shares of Nvidia."place_order

  • "Show my order history."get_order_history

To see authentication reject an unauthenticated call, start a fresh chat and ask for your portfolio before logging in — the tool returns a missing_token error and never runs its logic.


Authentication design

How the token flows

login verifies credentials and returns a signed JWT. Every other tool takes that token as a required parameter and verifies it as its first action. If verification fails, the tool returns a structured error and never touches the data layer.

Why a per-call token parameter rather than a session or a transport auth header:

  • It makes "verify on every call" literal and auditable. The security boundary is one line at the top of each tool (_authenticate(token)); there is no connection-level trust to reason about.

  • stdio has no per-request headers. Unlike HTTP, the stdio transport can't carry an Authorization header per call, so the token rides in the tool arguments.

  • It's stateless and portable. No server-side session store to manage or leak, and the exact same server works unchanged from any MCP client and would work over an HTTP transport too.

What "verify" means here

Verification uses jwt.decode(token, SECRET, algorithms=["HS256"]), which checks the signature and the exp (expiry) claim — it does not merely decode the payload. Each failure mode returns a distinct, structured error:

Situation

Error code

No token supplied

missing_token

Not a readable JWT

malformed_token

Signature doesn't match our secret (incl. forged tokens)

invalid_signature

Past its expiry

token_expired

Valid but missing sub

invalid_token

Other security properties

  • Secret management. JWT_SECRET comes from the environment (.env locally, which is git-ignored). The server raises on startup if it is unset.

  • Per-user isolation. The username is read from the verified token's sub claim, never from a caller-supplied argument. Every data-layer query is scoped by that username, so a valid token for demo can only ever act on demo's data.

  • Password storage. Passwords are bcrypt-hashed; plaintext is never stored.

  • No user enumeration. login returns the same error for a wrong password and a non-existent user.

Layout

auth.py     JWT sign/verify + bcrypt password hashing  (the entire security surface)
db.py       SQLite schema, seed data, and queries      (knows nothing about auth or MCP)
server.py   MCP tool definitions; the only place auth + data meet

auth.py is intentionally domain-agnostic — it contains no trading logic at all.


Testing

python -m pytest -q
  • tests/test_auth.py — the JWT core: password hashing, valid tokens, and every rejection path (missing, malformed, tampered, forged-with-wrong-secret, expired).

  • tests/test_tools.py — login, auth-gating on protected tools, successful authenticated trades, and graceful handling of bad input / domain violations.


Assumptions & trade-offs

Reasonable scope decisions for a take-home; called out for transparency.

  • Simulated market data. Prices come from a seeded SQLite table (with a small per-symbol day change), not a live feed. This keeps the project reproducible and key-free. A real feed could be added behind an env flag without changing any tool or auth code.

  • stdio transport. Chosen per the brief (HTTP/SSE is a bonus). With stdio, each user runs their own local instance, so the SQLite store is per-machine. A multi-user, internet-facing deployment would use the HTTP transport plus a shared database.

  • No token revocation / refresh. Tokens are valid until they expire (default 60 min). Revocation lists and refresh tokens were out of scope.

  • Market orders only, filled at the current price. No limit orders, slippage, or realized-P&L tracking; unrealized P&L is computed live in get_portfolio.

With more time

Remote HTTP transport + OAuth for use as a shared connector (incl. claude.ai Custom Connectors), token refresh/revocation, rate limiting on login, and limit-order support.


Demo

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Tejas-gaikwad/mcp_trade'

If you have feedback or need assistance with the MCP directory API, please join our Discord server