borgels

mcp-server-saxo

Community borgels
Updated

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 theBorgels 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 describewhat this server talks to. You need your own Saxo developer credentials, anduse 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 thisserver 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 ClientManagement beyond accounts/me are out of scope for v1.

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 letthe server refresh tokens for you. This is also the path you'll need forLIVE.

  1. Sign up at https://www.developer.saxo/.
  2. App Management → Create application (mark as SIM, allow tradingif you'll be placing orders). Pick either grant type — the serversupports both:
    • Code (confidential client, has an App Secret)
    • PKCE (public client, no secret — slightly safer fordistributable 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 rejectsIP-literal redirects like http://127.0.0.1:..., so use thehostname.
    • For PKCE apps: register http://localhost/callback — Saxo'sPKCE flow requires the registered URL to omit the port("When registering the redirect URL with your application, itcannot include a port number"). The server still sendshttp://localhost:8765/callback at runtime; Saxo matchesport-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, andthe CLI writes SAXO_ACCESS_TOKEN, SAXO_REFRESH_TOKEN, andSAXO_TOKEN_EXPIRES_AT back into .env. Both grant types use thesame flow; the server detects the absence of SAXO_APP_SECRET andswitches 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 ~60sbefore 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

Market data is a second, separate consent

The 24-hour token unlocks authenticated API access, but live bid/askquotes via /trade/v1/infoprices require a separate per-exchange marketdata 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. Onceaccepted, the same token starts returning quotes (typically DelayedByMinutes: 15unless the current OpenAPI session has TradeLevel: "FullTradingAndChat").DataLevel should stay at Saxo's default Standard; Saxo documents that ithas no impact for third-party applications.

Use saxo_get_session_capabilities to read TradeLevel directly, andsaxo_set_session_trade_level to switch between FullTradingAndChat andOrdersOnly. Only one session per Saxo user can have FullTradingAndChat, soupgrading this MCP session can downgrade SaxoTraderGO/PRO or another OpenAPIsession. saxo_diagnostics watches session events and flags TradeLevelproblems in warnings[]; infoprices responses are decorated with _warningwhen they're NoAccess.

Authentication

This server reads credentials from environment variables only — they arenever 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 Saxoauthorize endpoint with a PKCE challenge, and writesSAXO_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. Inpackaged clients such as Claude Desktop / MCPB this usually means clientconfig, not a local env file.
  2. For the smooth local flow, call saxo_oauth_login. It starts the loopbacklistener, opens the browser by default, waits for approval, exchanges thecode, and updates the running MCP server in memory.
  3. Tokens are not written to disk unless writeToEnvFile=true is supplied.Use envFilePath when you want a specific file such as .env.local.

For clients that want to control their own UI, use the lower-level two-stepflow: call saxo_oauth_start, open the returned authorizeUrl (or setopenBrowser=true), then call saxo_oauth_complete with the returnedticketId.

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

Install

The server is published as @borgels/mcp-server-saxo on npm andas an .mcpb bundle (MCP Bundle, the new name for DXT) on eachGitHub Release.The protocol layer is the same everywhere — stdio + MCP JSON-RPC. Thetable 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 durableuse (no daily token paste), do the OAuth dance once withnpm run auth -- --env sim against a clone of this repo, then copythe 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 decodesthe JWT exp, detects it's within 60s of expiry, and runs therefresh-token grant before sending the first request. The refreshtoken is what really matters at cold start.

Restart after editing

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

Troubleshooting

  • If the server doesn't appear in the client's tool list, check theclient's MCP logs (each client documents its log location).
  • Verify npx -y @borgels/mcp-server-saxo runs from a fresh shell — itshould start, register tools, and wait for stdin without error.
  • Once connected, call saxo_diagnostics first when something looksoff — its warnings[] array surfaces missing market-data terms,near-expiry tokens, TradeLevel not FullTradingAndChat, and LIVE order writes withoutSAXO_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 Saxotool to call. It returns tool descriptions, examples, identifier formats, andsafety 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 usingSaxo instruments, prices, chart bars, option chains, and account/positioncontext. If ALPHA_VANTAGE_API_KEY is set, stock and option strategy tools canoptionally enrich candidates with Alpha Vantage OVERVIEW,NEWS_SENTIMENT, and EARNINGS_CALENDAR.

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

Factor and portfolio context

Use saxo_screen_stock_factors when you want stock factor context with Saxoquote/liquidity data, Saxo chart context, optional account sizing, optionalfundamentals/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 underlyingsand 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 outputcontains factor scores and warnings, not pass/watchlist/reject verdicts orconfidence labels.

Use saxo_analyze_portfolio_context for whole-account context. It reads theaccount snapshot, runs the stock and option factor tools, then returns riskbudgets, stock/option factor summaries, concentration context, sector context,and warnings. It does not return target allocations, deployment stages, orrecommended 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 ortheses with deterministic discovery from Saxo market movers. If the Saxoaccount 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 andoption strategies against the plan you opened. Pass the executed stock leg oroption legs plus entry metrics from the selected plan or fill. The tool matchesthose legs to open Saxo positions, refreshes quotes, estimates current valueand P/L, and evaluates deterministic profit-taking/loss rules. Optionstrategies additionally include Greeks, theta, DTE, roll, and close rules. Itreturns 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 reasonabout safety. Read-only tools work on SIM and LIVE without extra opt-in. Writetools (orders, OAuth) follow the LIVE Trading Safetyrules.

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. Requiredfields: AccountKey, Uic, AssetType, BuySell, Amount, OrderType,OrderDuration. Optional: OrderPrice (Limit/StopLimit), StopPrice(Stop/StopLimit), ManualOrder, ExternalReference, and Orders[] forrelated 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. OrderTypemust be Limit. OrderPrice is always positive — the absolute limitprice you are willing to pay (debit spread) or receive (credit spread).Saxo's API rejects negative OrderPrice with "Price cannot benegative"; the debit/credit direction is implicit in each leg'sBuySell. Legs[] accepts 2–20 legs that all share the same optionroot (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) orsaxo_cancel_multileg_order (cancels the whole strategy) afterwards. Tofind the per-leg Uics, start with saxo_search_instruments for theunderlying, then saxo_get_option_chain to read off strikes andexpirations.

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 setSAXO_ENABLE_LIVE_ALERT_WRITES=true and set"allow_live_alert_writes": true in policy. This does not enable orderplacement.

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 Place-order automatically runs precheck first.
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 `

Even on SIM, all write tools run through the policy check (it just defaultsto 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 smallStreamable 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 to10 MiB, allows browser CORS only from loopback origins, and does not requirean HTTP Bearer token. Override with MCP_HTTP_HOST, MCP_MAX_BODY_BYTES,MCP_ALLOWED_ORIGINS, MCP_ALLOW_ANY_ORIGIN=true, and MCP_HTTP_TOKEN. TheMCP endpoint is POST http://127.0.0.1:3000/mcp.

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. Ongit push --tags vX.Y.Z:

  1. Typecheck, test, build, validate the MCPB manifest.
  2. Publish @borgels/[email protected] to npm with --provenanceso the package carries a SLSA attestation linking it to the exactCI 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 thebody and the .mcpb attached.

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

One-time setup before the first release

  1. Create the npm org. Athttps://www.npmjs.com/org/create, create the borgelsorganization (free, public packages).
  2. Bootstrap the first publish. Trusted Publishing (see below)can't be configured until the package exists on npm, so the veryfirst publish needs a token. Either:
    • Local one-shot: npm publish --access public with a one-timeAutomation Token logged in as a member of the borgels org. Runthis once from a clean checkout of the tagged commit. Revoke thetoken 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/accessTrusted publishing → Add publisher. Enter
    • Organization: Borgels
    • Repository: mcp-server-saxo
    • Workflow filename: release.yml
    • Environment: leave blank (or set to e.g. production if youwant to gate releases behind a GitHub Environment approval).
  4. Revoke the bootstrap token and delete the NPM_TOKEN reposecret. From this point on, every release authenticates viaGitHub Actions OIDC — no long-lived secrets anywhere.

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

Rate Limits

Saxo applies per-service-group rate limits (typically ~120 requests perminute per session per service group, and ~1 order per second). On 429 theserver preserves the retry-after header on the thrown SaxoHttpError socallers 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_URIis rejected at startup.
  • If SAXO_AUDIT_LOG is set, every tool call writes a JSONL line withtimestamp, request id, tool name, environment, action (start / finish/ error / policy_denied), SHA-256 hash of the input, status, andredacted error text. Raw inputs and tokens are not written.
  • Reports of suspected vulnerabilities go privately to[email protected]. Do not include credentials or personal data inpublic GitHub issues.

API Sources

License

Apache-2.0. See LICENSE.

MCP Server · Populars

MCP Server · New

    talivia-group

    Talivia Agent Kit

    Revenue-first website analytics installed and verified by AI agents through MCP

    Community talivia-group
    gura105

    Operational Ontology

    A minimal, readable reference implementation of the Operational Ontology pattern. Palantir Foundry is one implementation; this is the concept, minimized.

    Community gura105
    EllisMorrow

    Caelune

    Caelune (星野) — Local-first retrieval for private Markdown, PDF, and Tika documents, with a Windows desktop app and read-only MCP server.|本地优先的私人知识检索工具。

    Community EllisMorrow
    vmware-skills

    VMware AIops

    VMware vCenter/ESXi AI-powered monitoring and operations. Two skills: vmware-monitor (read-only, safe) and vmware-aiops (full operations) | Claude Code Skill

    Community vmware-skills
    asdecided

    AsDecided

    Native deterministic requirements-as-code engine and read-only MCP server.

    Community asdecided