Skip to main content
Glama
mcp-server-saxo

mcp-server-saxo

by borgels

mcp-server-saxo

TypeScript MCP server for the Saxo Bank OpenAPI. Works against both the SIM (simulation/demo) and LIVE environments. Same shape as the rest of the Borgels MCP server family: typed, documented, policy-aware, credential-sane, and audit-friendly.

Disclaimer: This is an independent, unofficial project by Borgels. Borgels is not affiliated with, endorsed by, or supported by Saxo Bank A/S. "Saxo", "Saxo Bank", and the Saxo OpenAPI are referenced only to describe what this server talks to. You need your own Saxo developer credentials, and use of the Saxo OpenAPI is subject to Saxo Bank's own terms and licensing. Trading on LIVE moves real money. You are responsible for any orders this server places on your behalf.

Scope

Supported Saxo OpenAPI service groups:

  • Root (session, diagnostics)

  • Reference Data (instruments, exchanges)

  • Trading (info prices, snapshot chart, order place / modify / cancel / precheck)

  • Portfolio (accounts, balances, positions, closed positions, orders)

  • Value Add (price alert definitions and notification settings)

Streaming subscriptions (WebSocket), Value Add performance, and Client Management beyond accounts/me are out of scope for v1.

Related MCP server: IBKR MCP Server

Quickstart on SIM

npm install
npm run build
cp .env.example .env

Then pick one of:

Path A — 24-hour token (quickest, one-shot)

  1. Sign up for a free SIM account at https://www.developer.saxo/.

  2. App Management → Generate 24-hour token (no app required).

  3. Paste it into .env:

    SAXO_ENVIRONMENT=sim
    SAXO_ACCESS_TOKEN=...
  4. Start the server (see Path A/B start block below).

The token expires after 24h; you'll need to repeat step 2 to keep going.

Path B — OAuth app (refreshes automatically)

For anything you run longer than a day, do the OAuth dance once and let the server refresh tokens for you. This is also the path you'll need for LIVE.

  1. Sign up at https://www.developer.saxo/.

  2. App Management → Create application (mark as SIM, allow trading if you'll be placing orders). Pick either grant type — the server supports both:

    • Code (confidential client, has an App Secret)

    • PKCE (public client, no secret — slightly safer for distributable clients but functionally equivalent for this server)

  3. Register the redirect URL in the portal:

    • For Code apps: register exactly http://localhost:8765/callback. Saxo matches the full URL including port at runtime, and rejects IP-literal redirects like http://127.0.0.1:..., so use the hostname.

    • For PKCE apps: register http://localhost/callback — Saxo's PKCE flow requires the registered URL to omit the port ("When registering the redirect URL with your application, it cannot include a port number"). The server still sends http://localhost:8765/callback at runtime; Saxo matches port-blind. Mismatching this returns unauthorized_client.

  4. Put the credentials in .env:

    SAXO_ENVIRONMENT=sim
    SAXO_APP_KEY=...
    SAXO_APP_SECRET=...        # Only for Code-grant apps. Omit for PKCE.
    SAXO_REDIRECT_URI=http://localhost:8765/callback
  5. Run the auth CLI:

    npm run auth -- --env sim

    This opens your browser to Saxo's authorize page, you click Allow, and the CLI writes SAXO_ACCESS_TOKEN, SAXO_REFRESH_TOKEN, and SAXO_TOKEN_EXPIRES_AT back into .env. Both grant types use the same flow; the server detects the absence of SAXO_APP_SECRET and switches to PKCE-style token exchange (no Authorization header, client_id in the form body).

The OAuth access token Saxo issues is short-lived (~20 minutes on SIM), but SaxoClient proactively refreshes it from the refresh token ~60s before expiry, so the server stays alive indefinitely.

Start the server

npm run dev          # stdio transport
# or
npm run dev:http     # http://127.0.0.1:3000/mcp

The 24-hour token unlocks authenticated API access, but live bid/ask quotes via /trade/v1/infoprices require a separate per-exchange market data agreement (NYSE, OPRA, EUREX, etc.). Until you accept it, saxo_get_infoprice returns PriceTypeAsk/Bid: "NoAccess" with Amount: 0, and saxo_session_me reports MarketDataViaOpenApiTermsAccepted: false.

There is no Saxo OpenAPI endpoint to flip this flag programmatically — it's a human consent screen. Find it in the Saxo trading platform (SaxoTraderGO → settings → live data subscriptions) or developer.saxo. Once accepted, the same token starts returning quotes (typically DelayedByMinutes: 15 unless the current OpenAPI session has TradeLevel: "FullTradingAndChat"). DataLevel should stay at Saxo's default Standard; Saxo documents that it has no impact for third-party applications.

Use saxo_get_session_capabilities to read TradeLevel directly, and saxo_set_session_trade_level to switch between FullTradingAndChat and OrdersOnly. Only one session per Saxo user can have FullTradingAndChat, so upgrading this MCP session can downgrade SaxoTraderGO/PRO or another OpenAPI session. saxo_diagnostics watches session events and flags TradeLevel problems in warnings[]; infoprices responses are decorated with _warning when they're NoAccess.

Authentication

This server reads credentials from environment variables only — they are never accepted as tool arguments.

Variable

Required for

Notes

SAXO_ENVIRONMENT

always

sim (default) or live

SAXO_ACCESS_TOKEN

always

Bearer token. 24-hour token for SIM, OAuth token for LIVE.

SAXO_REFRESH_TOKEN

LIVE / long-running SIM

Together with app credentials enables 401-auto-refresh.

SAXO_APP_KEY

refresh / OAuth

Application key from the developer portal.

SAXO_APP_SECRET

refresh / OAuth

Application secret.

SAXO_REDIRECT_URI

OAuth

Defaults to http://localhost:8765/callback. Loopback only — and Saxo's authorize endpoint rejects IP-literal redirects, so use the localhost hostname rather than 127.0.0.1. The URL in your app's Redirect URLs list must match exactly.

SAXO_TIMEOUT_MS

optional

Request timeout in ms (default 30000).

Two ways to log in for LIVE / long-running SIM

Option A — CLI (one-off, scriptable):

SAXO_APP_KEY=... SAXO_APP_SECRET=... npm run auth -- --env live

The CLI starts a local callback listener, opens your browser to the Saxo authorize endpoint with a PKCE challenge, and writes SAXO_ACCESS_TOKEN/SAXO_REFRESH_TOKEN/SAXO_TOKEN_EXPIRES_AT back into .env.

Option B — From inside the MCP client (saxo_oauth_* tools):

  1. Set SAXO_APP_KEY + SAXO_APP_SECRET in the MCP server environment. In packaged clients such as Claude Desktop / MCPB this usually means client config, not a local env file.

  2. For the smooth local flow, call saxo_oauth_login. It starts the loopback listener, opens the browser by default, waits for approval, exchanges the code, and updates the running MCP server in memory.

  3. Tokens are not written to disk unless writeToEnvFile=true is supplied, in which case they are written to .env in the server's working directory (the target path is not configurable from the tool call).

The saxo_oauth_* tools are only registered on the local stdio transport; the optional HTTP server does not expose them.

For clients that want to control their own UI, use the lower-level two-step flow: call saxo_oauth_start, open the returned authorizeUrl (or set openBrowser=true), then call saxo_oauth_complete with the returned ticketId.

The MCP server only listens on loopback (127.0.0.1) for the callback, so the flow never touches the public network beyond Saxo itself.

Install

The server is published as @borgels/mcp-server-saxo on npm and as an .mcpb bundle (MCP Bundle, the new name for DXT) on each GitHub Release. The protocol layer is the same everywhere — stdio + MCP JSON-RPC. The table below is just the per-client config syntax.

Client

How to install

Claude Desktop (1.8089+)

Download mcp-server-saxo-v<version>.mcpb from the latest Release, then Settings → Connectors → Install from file. Claude Desktop prompts you for the SAXO_* config values from the bundle's user_config schema.

Claude Desktop (legacy claude_desktop_config.json)

Add the universal JSON block to %APPDATA%\Claude\claude_desktop_config.json under mcpServers. Restart fully (File → Exit).

Claude Code (CLI)

claude mcp add saxo -- npx -y @borgels/mcp-server-saxo (or edit ~/.claude/mcp.json / per-project .mcp.json).

Cursor

Settings → MCP → Add server, or .cursor/mcp.json per project. Same JSON shape as below.

Codex (OpenAI)

~/.codex/config.toml: [mcp_servers.saxo] with command = "npx", args = ["-y", "@borgels/mcp-server-saxo"], env = { SAXO_ENVIRONMENT = "sim", ... }.

Windsurf / Cline / Zed / continue.dev

Settings UI, point at npx -y @borgels/mcp-server-saxo. Same JSON envelope.

MCP Inspector (debug)

npx @modelcontextprotocol/inspector npx -y @borgels/mcp-server-saxo

Self-host (any client)

command: "node", args: ["/absolute/path/to/dist/transports/stdio.js"]. Use double backslashes on Windows or forward slashes.

Universal config

For any client that uses the standard mcpServers config schema:

{
  "mcpServers": {
    "saxo": {
      "command": "npx",
      "args": ["-y", "@borgels/mcp-server-saxo"],
      "env": {
        "SAXO_ENVIRONMENT": "sim",
        "SAXO_ACCESS_TOKEN": "your-24h-sim-token"
      }
    }
  }
}

That's the minimal form — fine for a quick SIM test. For durable use (no daily token paste), do the OAuth dance once with npm run auth -- --env sim against a clone of this repo, then copy the resulting OAuth env vars into the block:

{
  "mcpServers": {
    "saxo": {
      "command": "npx",
      "args": ["-y", "@borgels/mcp-server-saxo"],
      "env": {
        "SAXO_ENVIRONMENT": "sim",
        "SAXO_APP_KEY": "...",
        "SAXO_APP_SECRET": "...",
        "SAXO_REFRESH_TOKEN": "...",
        "SAXO_ACCESS_TOKEN": "...",
        "SAXO_TOKEN_EXPIRES_AT": "2026-05-19T18:00:37.823Z",
        "SAXO_ENABLE_LIVE_TRADING": "false"
      }
    }
  }
}

SAXO_ACCESS_TOKEN may be expired at startup — SaxoClient decodes the JWT exp, detects it's within 60s of expiry, and runs the refresh-token grant before sending the first request. The refresh token is what really matters at cold start.

Restart after editing

Most MCP clients spawn server processes only at startup. After editing the client's config, fully quit and reopen it. Some clients (notably Claude Desktop) keep running in the system tray when the window is closed — make sure you actually exit before reopening.

Troubleshooting

  • If the server doesn't appear in the client's tool list, check the client's MCP logs (each client documents its log location).

  • Verify npx -y @borgels/mcp-server-saxo runs from a fresh shell — it should start, register tools, and wait for stdin without error.

  • Once connected, call saxo_diagnostics first when something looks off — its warnings[] array surfaces missing market-data terms, near-expiry tokens, TradeLevel not FullTradingAndChat, and LIVE order writes without SAXO_ENABLE_LIVE_TRADING.

Building from source (for hacking or before npm publish)

git clone https://github.com/Borgels/mcp-server-saxo.git
cd mcp-server-saxo
npm install
npm run build           # produces dist/transports/stdio.js
npm test

Use "command": "node", "args": ["/absolute/path/to/dist/transports/stdio.js"] in your client config to point at the built source. For the MCPB bundle, npm run mcpb:pack produces mcp-server-saxo.mcpb in the project root.

Start Here

Use saxo_capabilities first when an MCP client needs to decide which Saxo tool to call. It returns tool descriptions, examples, identifier formats, and safety notes without contacting Saxo.

{ "query": "place order", "limit": 5 }

Optional Alpha Vantage enrichment

The strategy screeners are Saxo-first. They work without third-party data using Saxo instruments, prices, chart bars, option chains, and account/position context. If ALPHA_VANTAGE_API_KEY is set, stock and option strategy tools can optionally enrich candidates with Alpha Vantage OVERVIEW, NEWS_SENTIMENT, and EARNINGS_CALENDAR.

Leave ALPHA_VANTAGE_API_KEY unset to keep the server Saxo-only. Alpha Vantage is optional because tiers and rate limits vary. For deeper research, run Alpha Vantage's own MCP server beside this Saxo server and pass normalized research into externalContextBySymbol.

Factor and portfolio context

Use saxo_screen_stock_factors when you want stock factor context with Saxo quote/liquidity data, Saxo chart context, optional account sizing, optional fundamentals/news enrichment, and warnings:

{
  "accountKey": "your-account-key",
  "market": "us",
  "universe": "large_cap",
  "objective": "balanced",
  "riskProfile": "balanced",
  "includeAccountContext": true,
  "includeFundamentalContext": true,
  "riskBudgetPercentPerIdea": 1,
  "maxSingleNamePercent": 10,
  "maxResults": 10
}

Use saxo_screen_option_strategy_factors when you want optionable underlyings and explicit strategy candidates with liquidity, structure, chart, IV/Greeks, optional news, and account sizing factors:

{
  "accountKey": "your-account-key",
  "market": "us_nasdaq",
  "underlyingUniverse": "auto",
  "strategies": ["put_credit_spread", "iron_condor"],
  "includeAccountContext": true,
  "riskBudgetPercent": 1,
  "requireGreeks": true,
  "maxThetaDailyPercentOfRisk": 1,
  "maxUnderlyings": 50,
  "maxUnderlyingScan": 500,
  "maxSymbolsToPlan": 5,
  "maxPlans": 10
}

Option candidate generation uses Saxo option-chain quotes, Saxo Greeks, optional OptionsChain IV context, and optional account-aware sizing. The output contains factor scores and warnings, not pass/watchlist/reject verdicts or confidence labels.

Use saxo_analyze_portfolio_context for whole-account context. It reads the account snapshot, runs the stock and option factor tools, then returns risk budgets, stock/option factor summaries, concentration context, sector context, and warnings. It does not return target allocations, deployment stages, or recommended contract counts:

{
  "accountKey": "your-account-key",
  "objective": "balanced_growth_income",
  "riskProfile": "balanced",
  "portfolioProfile": "concentrated_conviction",
  "deploymentStyle": "staged",
  "targetInvestedPercent": 80,
  "cashReservePercent": 10,
  "maxCashDollars": 1000,
  "maxSingleNamePercent": 10,
  "maxSectorPercent": 35,
  "maxOptionsRiskPercent": 5,
  "riskBudgetPercentPerIdea": 1,
  "maxSelectedUnderlyings": 5,
  "minPositionRiskDollars": 2500,
  "maxContractsPerPosition": 20,
  "fragmentationPolicy": "reject",
  "requireGreeks": true,
  "maxThetaDailyPercentOfRisk": 1,
  "stockUniverse": "large_cap",
  "stockMaxCandidates": 120,
  "discoverOptionCandidates": true,
  "optionDiscoveryUniverse": "auto",
  "optionTheses": [
    {
      "name": "Long-term stock replacement",
      "symbols": ["NVO"],
      "role": "core_conviction",
      "conviction": "high",
      "horizon": "leaps",
      "preferredStructures": ["long_call", "debit_spread"],
      "targetRiskPercent": 5
    }
  ],
  "includeStocks": true,
  "includeOptions": true
}

Set discoverOptionCandidates=true to blend user-supplied option symbols or theses with deterministic discovery from Saxo market movers. If the Saxo account is not approved for short option legs, set allowShortOptionLegs=false; short-leg structures are filtered from candidate generation.

Use saxo_review_strategy_positions after execution to monitor open stock and option strategies against the plan you opened. Pass the executed stock leg or option legs plus entry metrics from the selected plan or fill. The tool matches those legs to open Saxo positions, refreshes quotes, estimates current value and P/L, and evaluates deterministic profit-taking/loss rules. Option strategies additionally include Greeks, theta, DTE, roll, and close rules. It returns verdicts such as hold, review, consider_trim, consider_close, and roll_watch; execution remains a separate explicit order workflow.

Tools

All tools are registered with MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) so clients can reason about safety. Read-only tools work on SIM and LIVE without extra opt-in. Write tools (orders, OAuth) follow the LIVE Trading Safety rules.

Read-only

Tool

Endpoint

Purpose

saxo_capabilities

—

Discover tools without calling Saxo.

saxo_session_me

GET /port/v1/users/me

Authenticated user (Name, ClientKey, UserKey, MarketDataViaOpenApiTermsAccepted).

saxo_get_session_capabilities

GET /root/v1/sessions/capabilities

Current AuthenticationLevel, DataLevel, and TradeLevel without running diagnostics.

saxo_diagnostics

(aggregated)

Session + capabilities + token expiry + warnings (market-data terms, TradeLevel, token close to expiry).

saxo_feature_availability

GET /root/v1/features/availability

Inspect Saxo feature flags for News, Calendar, Gainers/Losers, and Chart.

saxo_search_instruments

GET /ref/v1/instruments

Search by keyword + asset type.

saxo_get_instrument_details

GET /ref/v1/instruments/details

Detailed metadata for one or many Uics.

saxo_list_exchanges

GET /ref/v1/exchanges

List exchanges (or one by ExchangeId).

saxo_get_option_chain

GET /ref/v1/instruments/contractoptionspaces/{optionRootId}

Strikes + expirations. normalize=true (default) pivots Put/Call into one row per strike.

saxo_list_option_expiries

(uses option chain)

Cheap helper: just the expiries (date, days, strike count) for an option root.

saxo_list_standard_option_expiries

GET /ref/v1/standarddates/optionexpiry

Standardized option-expiry calendar (3rd Friday monthlies, quarterlies, weeklies). Distinct from list_option_expiries.

saxo_find_option_leg

(composes search + chain)

Convenience helper: given symbol + expiry + strike + Call/Put, returns the leg Uic in one call instead of 4. Picks multi-leg-capable root when ambiguous.

saxo_get_infoprice

GET /trade/v1/infoprices

Snapshot bid/ask/last for one instrument. Adds _warning if PriceType=NoAccess.

saxo_get_infoprices_list

GET /trade/v1/infoprices/list

Snapshot prices for multiple Uics.

saxo_get_chart

GET /chart/v3/charts

Historical OHLC bars (horizon in minutes).

saxo_screen_market

Saxo instruments + info prices

User-friendly top gainers/losers and pre-market screeners.

saxo_compute_spread_quote

(uses infoprices)

Fetch bid/ask per leg and compute worst-case, mid, best-case net debit for a multi-leg spread.

saxo_estimate_vertical_spread

(pure math)

Given side + strikes + debit + contracts: max loss, max gain, breakeven, R/R. Applies 100x option multiplier.

saxo_generate_option_strategy_candidates

Option chain + prices

Explicit option strategy candidates with factor scores.

saxo_screen_option_strategy_factors

Market screener + chart TA + OptionsChain IV + planner

Cross-symbol option factor screening.

saxo_screen_stock_factors

Saxo instruments + prices + chart TA + optional fundamentals/news

Stock factor screening without verdicts.

saxo_analyze_portfolio_context

Account snapshot + stock/options factor tools

Whole-account factor, budget, and constraint context.

saxo_review_strategy_positions

Positions + quotes (+ Greeks for options)

Post-execution strategy follow-up with hold/trim/close/roll verdicts.

saxo_list_accounts

GET /port/v1/accounts/me

List the client's trading accounts.

saxo_get_balance

GET /port/v1/balances

Cash + margin balance.

saxo_list_positions

GET /port/v1/positions/me

Open positions (one row per fill).

saxo_list_net_positions

GET /port/v1/netpositions/me

Positions aggregated per instrument (one row per Uic). Right view for current exposure.

saxo_list_closed_positions

GET /port/v1/closedpositions/me

Closed positions / history.

saxo_list_activities

GET /port/v1/activities

Recent account events: orders placed/modified/cancelled, trades, dividends, corporate actions.

saxo_list_orders

GET /port/v1/orders/me

Working orders.

saxo_get_order

GET /port/v1/orders/{orderId}

One order by id.

saxo_list_price_alerts

GET /vas/v1/pricealerts/definitions

List Saxo price alerts, optionally by state.

saxo_get_price_alert

GET /vas/v1/pricealerts/definitions/{id}

One price alert definition by id.

saxo_get_price_alert_user_settings

GET /vas/v1/pricealerts/usersettings

Price-alert email/popup/sound settings.

Write — guarded

Tool

Endpoint

Guards

saxo_precheck_order

POST /trade/v2/orders/precheck

Policy + audit. No execution.

saxo_place_order

POST /trade/v2/orders

LIVE: SAXO_ENABLE_LIVE_TRADING=true + policy.json allow + optional auto-precheck.

saxo_modify_order

PATCH /trade/v2/orders

Same as place_order.

saxo_cancel_order

DELETE /trade/v2/orders/{ids}

Policy + audit.

saxo_precheck_multileg_order

POST /trade/v2/orders/multileg/precheck

Validate a spread (no execution).

saxo_place_multileg_order

POST /trade/v2/orders/multileg

Place a spread atomically with a single net debit/credit limit.

saxo_modify_multileg_order

PATCH /trade/v2/orders/multileg

Adjust spread Amount or OrderPrice.

saxo_cancel_multileg_order

DELETE /trade/v2/orders/multileg/{id}

Cancel the whole strategy.

saxo_create_price_alert

POST /vas/v1/pricealerts/definitions

LIVE alert writes require SAXO_ENABLE_LIVE_ALERT_WRITES=true + policy allow.

saxo_update_price_alert

PUT /vas/v1/pricealerts/definitions/{id}

Partial tool input is merged with the current definition before PUT.

saxo_delete_price_alerts

DELETE /vas/v1/pricealerts/definitions/{ids}

Delete one or more price alerts.

saxo_update_price_alert_user_settings

PUT /vas/v1/pricealerts/usersettings

Update email/popup/sound settings.

saxo_set_session_trade_level

PATCH /root/v1/sessions/capabilities

LIVE requires policy.allow_live_session_capability_writes=true; can downgrade other Saxo sessions.

saxo_oauth_login

OAuth2 PKCE

One-call local login; updates in-process tokens. Optional env-file persist.

saxo_oauth_start

OAuth2 PKCE

Loopback redirect only; reads app creds from env.

saxo_oauth_complete

OAuth2 PKCE

Replaces in-process tokens. Optional .env persist.

saxo_oauth_cancel

—

Closes a pending OAuth listener.

Place / modify order body

Order tools accept Saxo's standard POST /trade/v2/orders body. Required fields: AccountKey, Uic, AssetType, BuySell, Amount, OrderType, OrderDuration. Optional: OrderPrice (Limit/StopLimit), StopPrice (Stop/StopLimit), ManualOrder, ExternalReference, and Orders[] for related orders (OCO, IfDone, brackets).

{
  "AccountKey": "your-account-key",
  "Uic": 211,
  "AssetType": "Stock",
  "BuySell": "Buy",
  "Amount": 1,
  "OrderType": "Market",
  "OrderDuration": { "DurationType": "DayOrder" }
}

Multi-leg option order body

Multi-leg tools wrap Saxo's /trade/v2/orders/multileg family. OrderType must be Limit. OrderPrice is always positive — the absolute limit price you are willing to pay (debit spread) or receive (credit spread). Saxo's API rejects negative OrderPrice with "Price cannot be negative"; the debit/credit direction is implicit in each leg's BuySell. Legs[] accepts 2–20 legs that all share the same option root (same underlying + expiry).

{
  "AccountKey": "your-account-key",
  "OrderType": "Limit",
  "OrderPrice": 1.08,
  "OrderDuration": { "DurationType": "GoodTillCancel" },
  "ManualOrder": true,
  "ExternalReference": "bull-call-spread-1",
  "Legs": [
    {
      "Uic": 14853018,
      "AssetType": "StockOption",
      "BuySell": "Buy",
      "Amount": 150,
      "ToOpenClose": "ToOpen"
    },
    {
      "Uic": 14853056,
      "AssetType": "StockOption",
      "BuySell": "Sell",
      "Amount": 150,
      "ToOpenClose": "ToOpen"
    }
  ]
}

Saxo returns a MultiLegOrderId plus per-leg Orders[].OrderId values. Use saxo_modify_multileg_order (Amount/OrderPrice only) or saxo_cancel_multileg_order (cancels the whole strategy) afterwards. To find the per-leg Uics, start with saxo_search_instruments for the underlying, then saxo_get_option_chain to read off strikes and expirations.

LIVE Trading Safety

LIVE writes are denied by default. To enable them you must do all three:

  1. Set SAXO_ENVIRONMENT=live.

  2. Set SAXO_ENABLE_LIVE_TRADING=true.

  3. Point SAXO_POLICY_PATH at a policy.json that sets "allow_live_writes": true.

Price-alert writes are separate from trading writes. To create, update, delete, or change notification settings on LIVE you must set SAXO_ENABLE_LIVE_ALERT_WRITES=true and set "allow_live_alert_writes": true in policy. This does not enable order placement.

A copy of policy.example.json is included. Supported fields:

Field

Effect

allow_live_writes

Master switch for all order writes on LIVE.

allow_live_alert_writes

Master switch for price-alert create/update/delete/settings writes on LIVE.

allow_live_session_capability_writes

Allows changing LIVE session TradeLevel (FullTradingAndChat / OrdersOnly).

require_precheck_on_live

On LIVE, place/modify (single and multi-leg) run a Saxo precheck first and abort unless it returns PreCheckResult: Ok with no ErrorInfo.

allow_short_option_legs

Set to false when the Saxo option profile does not permit opening short option legs; multi-leg write tools then block sell-to-open option legs before calling Saxo.

allowed_asset_types

Whitelist of AssetTypes that may be ordered.

allowed_account_keys

Whitelist of AccountKeys that may be traded.

denied_uics

Blocklist of Uics.

max_order_amount

Per-AssetType caps; default falls back when no specific entry.

max_notional

Cap on Amount * (OrderPrice or StopPrice) * contract_multiplier. Multiplier is 100 for StockOption / IndexOption / StockIndexOption / FuturesOption, 1 otherwise. For multi-leg spreads, applied as `

saxo_modify_order / saxo_modify_multileg_order load the existing order from Saxo and evaluate the policy against the real order merged with the requested changes; a mismatching AccountKey/Uic/AssetType is refused.

Even on SIM, all write tools run through the policy check (it just defaults to permissive). Use the policy in SIM too if you want predictable limits.

Optional HTTP Server

The local stdio transport is the default for agent compatibility. A small Streamable HTTP entry point is also available:

PORT=3000 SAXO_ACCESS_TOKEN=... npm run dev:http

By default the HTTP server binds to 127.0.0.1, limits request bodies to 10 MiB, allows browser CORS only from loopback origins, and does not require an HTTP Bearer token. Override with MCP_HTTP_HOST, MCP_MAX_BODY_BYTES, MCP_ALLOWED_ORIGINS, MCP_ALLOW_ANY_ORIGIN=true, and MCP_HTTP_TOKEN (compared in constant time). The MCP endpoint is POST http://127.0.0.1:3000/mcp. The saxo_oauth_* tools are not available over HTTP.

Verification

npm run typecheck
npm test
npm run build

Optional live SIM smoke test (requires a 24-hour SIM token):

SAXO_ACCESS_TOKEN="your-sim-token" npm run smoke:live

Releasing

Tagged releases trigger .github/workflows/release.yml. On git push --tags vX.Y.Z:

  1. Typecheck, test, build, validate the MCPB manifest.

  2. Publish @borgels/mcp-server-saxo@X.Y.Z to npm with --provenance so the package carries a SLSA attestation linking it to the exact CI run + commit.

  3. Reinstall with --omit=dev and pack a small .mcpb bundle (~2.6 MB; full dev install yields ~17 MB).

  4. Create a GitHub Release with the matching CHANGELOG section as the body and the .mcpb attached.

Pre-release tags (e.g. v0.1.1-rc.1) publish under the next npm dist-tag and as a GitHub pre-release.

One-time setup before the first release

  1. Create the npm org. At https://www.npmjs.com/org/create, create the borgels organization (free, public packages).

  2. Bootstrap the first publish. Trusted Publishing (see below) can't be configured until the package exists on npm, so the very first publish needs a token. Either:

    • Local one-shot: npm publish --access public with a one-time Automation Token logged in as a member of the borgels org. Run this once from a clean checkout of the tagged commit. Revoke the token immediately afterwards.

    • CI bootstrap: add the token as the NPM_TOKEN repo secret, push v0.1.1-rc.1, let CI publish, then delete the secret.

  3. Configure Trusted Publishing (one-time, per package): https://www.npmjs.com/package/@borgels/mcp-server-saxo/access → Trusted publishing → Add publisher. Enter

    • Organization: Borgels

    • Repository: mcp-server-saxo

    • Workflow filename: release.yml

    • Environment: leave blank (or set to e.g. production if you want to gate releases behind a GitHub Environment approval).

  4. Revoke the bootstrap token and delete the NPM_TOKEN repo secret. From this point on, every release authenticates via GitHub Actions OIDC — no long-lived secrets anywhere.

The same three steps repeat once per sibling Borgels MCP server when they migrate to this template.

Rate Limits

Saxo applies per-service-group rate limits (typically ~120 requests per minute per session per service group, and ~1 order per second). On 429 the server preserves the retry-after header on the thrown SaxoHttpError so callers can back off.

Security And Audit

  • All credentials (SAXO_ACCESS_TOKEN, SAXO_REFRESH_TOKEN, SAXO_APP_KEY, SAXO_APP_SECRET) are read only from the MCP server environment.

  • Credentials are never accepted as tool arguments.

  • Error formatting and audit records redact Authorization: Bearer ..., access_token, refresh_token, and SAXO_APP_SECRET style material.

  • The OAuth listener only binds to loopback. Non-loopback SAXO_REDIRECT_URI is rejected at startup.

  • If SAXO_AUDIT_LOG is set, every tool call writes a JSONL line with timestamp, request id, tool name, environment, action (start / finish / error / policy_denied), SHA-256 hash of the input, status, and redacted error text. Raw inputs and tokens are not written.

  • Reports of suspected vulnerabilities go privately to security@borgels.com. Do not include credentials or personal data in public GitHub issues.

API Sources

License

Apache-2.0. See LICENSE.

Available Tools

51 tools
saxo_analyze_portfolio_contextAnalyze Portfolio ContextB
Read-onlyIdempotent

Read-only whole-account context analyzer. Combines account snapshot, stock factors, option factors, risk budgets, concentration context, and warnings without allocation or deployment recommendations.

Inputs: accountKey (required), objective, riskProfile, portfolioProfile, +40 more.

saxo_cancel_multileg_orderCancel Multi-Leg Option OrderA
Destructive

Cancel a working multi-leg order. Cancels the whole strategy — individual legs cannot be cancelled separately.

Inputs: multiLegOrderId (required), accountKey (required).

saxo_cancel_orderCancel OrderA
Destructive

Cancel one or more working orders. LIVE writes require SAXO_ENABLE_LIVE_TRADING=true.

Inputs: orderIds (required), accountKey (required).

saxo_capabilitiesSearch Saxo CapabilitiesA
Read-onlyIdempotent

Search the Saxo MCP server capabilities and examples. Use this first when deciding which Saxo tool to call.

Inputs: query, limit.

saxo_compute_spread_quoteCompute Spread QuoteA
Read-onlyIdempotent

Fetch live bid/ask for each leg of a multi-leg option strategy and compute the worst-case, best-case, and mid net debit. Result is positive when the strategy is a net debit (you pay), negative when it is a net credit (you receive). Surfaces NoAccess warnings per leg when market-data terms are missing.

Inputs: legs (required), accountKey.

saxo_create_price_alertCreate Price AlertB
Destructive

Create a Saxo price alert definition. LIVE alert writes require SAXO_ENABLE_LIVE_ALERT_WRITES=true plus policy.allow_live_alert_writes=true.

Inputs: AccountId (required), Uic (required), AssetType (required), TargetValue (required), +7 more.

saxo_delete_price_alertsDelete Price AlertsA
Destructive

Delete one or more Saxo price alert definitions. LIVE alert writes require SAXO_ENABLE_LIVE_ALERT_WRITES=true plus policy.allow_live_alert_writes=true.

Inputs: alertDefinitionIds (required).

saxo_diagnosticsSaxo OpenAPI DiagnosticsA
Read-onlyIdempotent

Hit the Saxo diagnostics endpoint to verify connectivity.

Inputs: no named parameters.

saxo_estimate_vertical_spreadEstimate Vertical Spread RiskA
Read-onlyIdempotent

Pure math: given side (BullCall/BearCall/BullPut/BearPut), longStrike, shortStrike, debit (negative for credit spreads), and contracts, returns max loss, max gain, and breakeven in account currency, applying the option contract multiplier (100 for US equity options).

Inputs: side (required), longStrike (required), shortStrike (required), debit (required), +2 more.

saxo_feature_availabilityGet Saxo Feature AvailabilityA
Read-onlyIdempotent

Return Saxo feature flags for News, Calendar, Gainers/Losers, and Chart. Diagnostic only: availability flags do not guarantee that every feature has a public documented endpoint exposed by this MCP server.

Inputs: no named parameters.

saxo_find_option_legFind Option Leg by Symbol/Expiry/StrikeA
Read-onlyIdempotent

Convenience helper that resolves an option leg Uic from human-readable parameters (symbol + expiry + strike + Call/Put). Compresses the 4-step option-discovery workflow (search instrument → search option root → fetch chain → locate strike) into one call. Useful before saxo_place_order / saxo_place_multileg_order. When multiple option roots match (e.g. ADR vs. local listing), prefers the multi-leg-capable root and surfaces alternatives in warnings[]; pass exchangeId to disambiguate.

Inputs: symbol (required), expiry (required), strike (required), putCall (required), +1 more.

saxo_generate_option_strategy_candidatesGenerate Option Strategy CandidatesA
Read-onlyIdempotent

Read-only option candidate generator for explicit caller-provided strategies. Returns structures, legs, pricing, Greeks, and factor context; does not choose a playbook, call precheck, or place orders.

Inputs: strategies (required), keywords, optionRootId, accountKey, +17 more.

saxo_get_balanceGet Account BalanceC
Read-onlyIdempotent

Fetch the cash + margin balance for an account.

Inputs: accountKey, clientKey.

saxo_get_chartGet Chart (Historical OHLC)B
Read-onlyIdempotent

Fetch historical OHLC bars for an instrument. Horizon is in minutes (1, 5, 60, 1440 ...). Count defaults to Saxo default (max 1200).

Inputs: uic (required), assetType (required), horizon (required), count, +3 more.

saxo_get_infopriceGet Snapshot PriceA
Read-onlyIdempotent

Fetch a snapshot bid/ask/last price for a single instrument. Snapshot only — no subscription side effects.

Inputs: uic (required), assetType (required), accountKey, amount, +1 more.

saxo_get_infoprices_listGet Snapshot Prices (List)B
Read-onlyIdempotent

Fetch snapshot prices for multiple Uics in one call.

Inputs: uics (required), assetType (required), accountKey, fieldGroups.

saxo_get_instrument_detailsGet Instrument DetailsB
Read-onlyIdempotent

Fetch detailed metadata for one or more instruments by Uic + AssetType.

Inputs: uics (required), assetType (required), accountKey, fieldGroups.

saxo_get_option_chainGet Option ChainA
Read-onlyIdempotent

Fetch the option chain (strikes + expirations) for an option root. Use this after saxo_search_instruments with assetTypes=[StockOption] to find the Uic of each option leg before placing a multi-leg spread. Set normalize=true (default) to return one row per strike with callUic+putUic; normalize=false returns the raw Saxo OptionSpace shape.

Inputs: optionRootId (required), expiryDates, strikeCount, clientKey, +3 more.

saxo_get_orderGet OrderB
Read-onlyIdempotent

Fetch a specific order by OrderId.

Inputs: orderId (required), clientKey, fieldGroups.

saxo_get_price_alertGet Price AlertA
Read-onlyIdempotent

Fetch one Saxo price alert definition by AlertDefinitionId.

Inputs: alertDefinitionId (required).

saxo_get_price_alert_user_settingsGet Price Alert Notification SettingsA
Read-onlyIdempotent

Read the current user price-alert notification settings (email, popup, sound).

Inputs: no named parameters.

saxo_get_session_capabilitiesGet Saxo Session CapabilitiesA
Read-onlyIdempotent

Return current Saxo session capabilities, including TradeLevel and DataLevel, without running diagnostics.

Inputs: no named parameters.

saxo_list_accountsList AccountsB
Read-onlyIdempotent

List the authenticated client's trading accounts.

Inputs: clientKey, includeSubAccounts.

saxo_list_activitiesList Account ActivitiesA
Read-onlyIdempotent

Recent account events from /port/v1/activities — placed/modified/cancelled orders, trades, dividend payments, corporate actions. Pass fromDateTime/toDateTime (ISO 8601 with timezone) to scope; defaults to a recent window on Saxo side. Useful for "what happened on my account today?" reasoning.

Inputs: clientKey, accountKey, fromDateTime, toDateTime, +3 more.

saxo_list_closed_positionsList Closed PositionsC
Read-onlyIdempotent

List closed positions / trade history.

Inputs: clientKey, accountKey, fromDate, toDate, +2 more.

saxo_list_exchangesList ExchangesB
Read-onlyIdempotent

List Saxo-supported exchanges, or fetch one by ExchangeId.

Inputs: exchangeId, top, skip.

saxo_list_net_positionsList Net Positions (Aggregated)A
Read-onlyIdempotent

List positions aggregated per instrument (one row per Uic with the net amount), rather than per individual fill. Right view for "what is my current exposure?" — no manual deduplication needed.

Inputs: clientKey, accountKey, fieldGroups, top, +1 more.

saxo_list_option_expiriesList Option ExpiriesB
Read-onlyIdempotent

Cheap helper that returns just the available expiries for an option root: expiry date, days-to-expiry, last trade date, and strike count. Use to pick an expiry before pulling the full chain.

Inputs: optionRootId (required), clientKey, accountKey, trading.

saxo_list_ordersList OrdersB
Read-onlyIdempotent

List working orders for the authenticated client or a specific account.

Inputs: clientKey, accountKey, status, fieldGroups, +2 more.

saxo_list_positionsList Open PositionsA
Read-onlyIdempotent

List open positions for the authenticated client or a specific account. Returns one row per position (multiple rows per instrument if filled at different prices). Use saxo_list_net_positions for the per-instrument aggregated view.

Inputs: clientKey, accountKey, fieldGroups, top, +1 more.

saxo_list_price_alertsList Price AlertsB
Read-onlyIdempotent

List Saxo price alert definitions for the current user, optionally filtered by state.

Inputs: inlinecount, skip, top, state.

saxo_list_standard_option_expiriesList Standard Option Expiry DatesA
Read-onlyIdempotent

Return the standardized option-expiry calendar (3rd Friday monthlies, quarterlies, weeklies) from Saxo reference data. Useful for "is 2027-01-15 a standard monthly?" reasoning. For per-option-root expiries, use saxo_list_option_expiries instead.

Inputs: fromDate.

saxo_modify_multileg_orderModify Multi-Leg Option OrderA
Destructive

Modify a working multi-leg order. Only Amount (scaled symmetrically across legs) and OrderPrice can be changed.

Inputs: AccountKey (required), MultiLegOrderId (required), Amount, OrderPrice.

saxo_modify_orderModify OrderB
Destructive

Modify a working order (amount, price, duration). Same LIVE guards as saxo_place_order.

Inputs: OrderId (required), AccountKey (required), Uic (required), AssetType (required), +5 more.

saxo_oauth_cancelCancel Saxo OAuth LoginA
Destructive

Cancel a pending OAuth login flow (closes the callback listener).

Inputs: ticketId (required).

saxo_oauth_completeComplete Saxo OAuth LoginA
Destructive

Wait for the Saxo callback, exchange the code for tokens, and update the running MCP server. Optionally writes tokens to a .env file.

Inputs: ticketId (required), timeoutSeconds, writeToEnvFile, envFilePath.

saxo_oauth_loginRun Saxo OAuth LoginA
Destructive

Run the full Saxo OAuth2 + PKCE login in one MCP call. Starts a loopback callback listener, optionally opens the browser, waits for approval, exchanges tokens, updates the running MCP server, and optionally persists tokens to an env file.

Inputs: environment, timeoutSeconds, openBrowser, writeToEnvFile, +1 more.

saxo_oauth_startStart Saxo OAuth LoginA
Destructive

Begin a Saxo OAuth2 + PKCE login. Requires SAXO_APP_KEY + SAXO_APP_SECRET in the MCP server environment. Returns a ticketId and an authorizeUrl, optionally opening it in the browser. Then call saxo_oauth_complete with the ticketId.

Inputs: environment, openBrowser.

saxo_place_multileg_orderPlace Multi-Leg Option OrderA
Destructive

Place a multi-leg option strategy as one atomic order with a single limit price. OrderType must be Limit. OrderPrice is always positive — the absolute price you are willing to pay (debit) or receive (credit); Saxo infers direction from the legs. All legs must share the same option root (same underlying + expiry). Returns MultiLegOrderId plus per-leg OrderIds.

Inputs: AccountKey (required), OrderType (required), OrderDuration (required), Legs (required), +3 more.

saxo_place_orderPlace OrderA
Destructive

Place a new Saxo order. Defaults to SIM. LIVE writes require SAXO_ENABLE_LIVE_TRADING=true plus a policy.json that sets allow_live_writes=true. Policy may also cap Amount/AssetType/AccountKey/Uic/notional.

Inputs: AccountKey (required), Uic (required), AssetType (required), BuySell (required), +9 more.

saxo_precheck_multileg_orderPrecheck Multi-Leg Option OrderA
Destructive

Validate a multi-leg option strategy (vertical/calendar spread, condor, straddle, etc.) without placing it. OrderType must be Limit; OrderPrice is always positive — the absolute limit price you are willing to pay (debit spreads) or receive (credit spreads). Saxo infers debit vs credit from the Buy/Sell direction of the legs and rejects negative OrderPrice with "Price cannot be negative." All legs must share the same option root.

Inputs: AccountKey (required), OrderType (required), OrderDuration (required), Legs (required), +3 more.

saxo_precheck_orderPrecheck OrderC
Destructive

Validate an order against Saxo (margin, prices, instrument rules) without placing it. Runs through the policy + audit even though no execution happens.

Inputs: AccountKey (required), Uic (required), AssetType (required), BuySell (required), +9 more.

saxo_review_strategy_positionsReview Strategy PositionsA
Read-onlyIdempotent

Read-only follow-up review for executed stock and option strategies. Matches expected legs to open positions, refreshes quotes, adds Greeks/DTE for options, evaluates P/L, trim/close/roll rules, and returns deterministic decision support. Does not precheck or place orders.

Inputs: accountKey, clientKey, strategySnapshotPath, reviewDepth, +12 more.

saxo_screen_marketScreen MarketA
Read-onlyIdempotent

User-friendly read-only market screener for presets like top gainers, top losers, pre-market gainers, and pre-market losers. Uses Saxo instruments and InfoPrices only; output depends on market-data permissions and delay settings.

Inputs: preset (required), market, exchangeIds, assetType, +4 more.

saxo_screen_option_strategy_factorsScreen Option Strategy FactorsA
Read-onlyIdempotent

Read-only factor screener for explicit option strategies across symbols or Saxo market movers. Returns candidate structures, liquidity, chart, IV/Greeks, optional news, and sizing context without verdicts or confidence labels.

Inputs: strategies (required), accountKey, market, symbols, +30 more.

saxo_screen_stock_factorsScreen Stock FactorsA
Read-onlyIdempotent

Read-only stock factor screener with Saxo quotes, chart context, optional account sizing, and optional Alpha Vantage fundamentals/news. Returns factors and warnings without verdicts or confidence labels.

Inputs: accountKey, market, symbols, excludeSymbols, +21 more.

saxo_search_instrumentsSearch InstrumentsA
Read-onlyIdempotent

Search Saxo reference data for instruments by keyword and asset type. Returns matching instruments with Uic and AssetType (use those as input to other tools).

Inputs: keywords, assetTypes, exchangeIds, accountKey, +3 more.

saxo_session_meGet Saxo SessionA
Read-onlyIdempotent

Return the current Saxo session (ClientKey, UserKey, default account, culture). Useful to verify the access token works.

Inputs: no named parameters.

saxo_set_session_trade_levelSet Saxo Session TradeLevelA
Destructive

Set session TradeLevel to FullTradingAndChat or OrdersOnly and return the confirmed session capabilities. LIVE requires policy.allow_live_session_capability_writes=true.

Inputs: tradeLevel (required), confirmTimeoutMs.

saxo_update_price_alertUpdate Price AlertA
Destructive

Update an existing Saxo price alert definition. Partial input is merged with the current alert before PUT because Saxo expects the full definition body.

Inputs: AlertDefinitionId (required), AccountId, Uic, AssetType, +8 more.

saxo_update_price_alert_user_settingsUpdate Price Alert Notification SettingsB
Destructive

Update the current user price-alert notification settings. Partial input is merged with current settings before PUT.

Inputs: EmailAddress, NotifyWithMail, NotifyWithPopup, Sound.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 51 tools

    Disambiguation5/5

    Each tool targets a distinct function or resource type. Tools like list_positions vs list_net_positions, place_order vs place_multileg_order, and the various option-related tools are clearly differentiated by their descriptions, leaving no ambiguity.

    Naming Consistency5/5

    All tools follow a consistent 'saxo_verb_noun' pattern in lowercase snake_case. Verbs are chosen appropriately and nouns specify the target, making the naming predictable and easy to navigate.

    Tool Count2/5

    With 51 tools, the count exceeds 25, which per the rubric is considered too many for the apparent scope. While the server covers a complex trading domain, the tool surface feels bloated and could benefit from consolidation.

    Completeness5/5

    The tool set covers the full lifecycle of trading interactions: account management, order placement (single and multi-leg), prechecks, positions, market screening, alerts, OAuth, session management, and diagnostics. No obvious gaps are apparent for the stated purpose.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables trading and portfolio management through the Alpaca API, allowing users to place orders, manage positions and watchlists, access market data, and retrieve account information through natural language.
      22 npm
      4
      ISC
    • A
      license
      Not graded
      quality
      D
      maintenance
      Provides AI models with secure access to Interactive Brokers trading data and functionality, enabling account management, market data retrieval, and trading operations through natural language interactions.
      18
      MIT
    • A
      license
      Not graded
      quality
      F
      maintenance
      Enables natural language trading operations through Alpaca's API, supporting stocks, options, crypto, portfolio management, and real-time market data with comprehensive order execution and account management capabilities.
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Enables AI assistants to interact with Trading 212 investment accounts for portfolio tracking, account management, and real-time order execution. It supports managing investment pies, analyzing historical data, and monitoring market performance across multiple instrument types.
      23
      2
      MIT