{"_id":"@borgels/mcp-server-saxo","_rev":"4-ea80bcaa8b49a69a57fc0fac36d57d05","name":"@borgels/mcp-server-saxo","dist-tags":{"next":"0.2.0-rc.1","latest":"0.2.1"},"versions":{"0.1.1":{"name":"@borgels/mcp-server-saxo","version":"0.1.1","keywords":["mcp","model-context-protocol","saxo","saxobank","trading","openapi","borgels"],"author":{"name":"Borgels"},"license":"Apache-2.0","_id":"@borgels/mcp-server-saxo@0.1.1","maintainers":[{"name":"abolsen","email":"abo@borgels.com"}],"homepage":"https://github.com/Borgels/mcp-server-saxo#readme","bugs":{"url":"https://github.com/Borgels/mcp-server-saxo/issues"},"bin":{"mcp-server-saxo":"dist/transports/stdio.js"},"dist":{"shasum":"b972f80dfed9e3cc90fee556a0e5663db7c390f8","tarball":"https://registry.npmjs.org/@borgels/mcp-server-saxo/-/mcp-server-saxo-0.1.1.tgz","fileCount":20,"integrity":"sha512-aamcejFv3cBOBnVdE1bfIjR5DenjcJ8JFTnoXl/phQBJLDoVY+hy5AFXK/wdzAKRBNfSmvTSuye0tWVXggD+pA==","signatures":[{"sig":"MEQCID4ADEnovWtFRTDeVwPEA4LiUhbHMYUUtckyx5R1WID8AiB/bo8WhgRKyprgBxKmQ+2h3lrhw3Nq7UGi7N5oN70SbA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":345670},"main":"./dist/transports/stdio.js","type":"module","types":"./dist/transports/stdio.d.ts","engines":{"node":">=20.11"},"scripts":{"dev":"tsx src/transports/stdio.ts","auth":"tsx src/bin/auth.ts","test":"vitest run","build":"tsup src/transports/stdio.ts src/transports/http.ts src/bin/auth.ts --format esm --dts --clean --sourcemap","prepack":"npm run typecheck && npm test && npm run build","dev:http":"tsx src/transports/http.ts","mcpb:pack":"mcpb pack . mcp-server-saxo.mcpb","typecheck":"tsc --noEmit","smoke:live":"tsx test/live-smoke.ts","mcpb:validate":"mcpb validate manifest.json"},"_npmUser":{"name":"abolsen","email":"abo@borgels.com"},"overrides":{"hono":">=4.12.16","fast-uri":">=3.1.2","ip-address":">=10.1.1"},"repository":{"url":"git+https://github.com/Borgels/mcp-server-saxo.git","type":"git"},"_npmVersion":"11.12.1","description":"MCP server for the Saxo Bank OpenAPI (SIM and LIVE trading).","directories":{},"_nodeVersion":"24.15.0","dependencies":{"zod":"^4.4.3","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.21.0","tsup":"^8.5.1","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.7.0","@anthropic-ai/mcpb":"^2.1.2"},"_npmOperationalInternal":{"tmp":"tmp/mcp-server-saxo_0.1.1_1779218309967_0.10008008723907569","host":"s3://npm-registry-packages-npm-production"}},"0.2.0-rc.1":{"name":"@borgels/mcp-server-saxo","version":"0.2.0-rc.1","keywords":["mcp","model-context-protocol","saxo","saxobank","trading","openapi","borgels"],"author":{"name":"Borgels"},"license":"Apache-2.0","_id":"@borgels/mcp-server-saxo@0.2.0-rc.1","maintainers":[{"name":"abolsen","email":"abo@borgels.com"}],"homepage":"https://github.com/Borgels/mcp-server-saxo#readme","bugs":{"url":"https://github.com/Borgels/mcp-server-saxo/issues"},"bin":{"mcp-server-saxo":"dist/transports/stdio.js"},"dist":{"shasum":"8f0da692ded8bb9d22be3e560e341777a1233c8d","tarball":"https://registry.npmjs.org/@borgels/mcp-server-saxo/-/mcp-server-saxo-0.2.0-rc.1.tgz","fileCount":20,"integrity":"sha512-94nSXmbEXUk8PqlW1GXFlTu88veuvqd+LPh7ZvblIOPW2Wh19yhYncEoIylSricFBF3vTxGvTh9VL/+lPnsw3Q==","signatures":[{"sig":"MEUCIDOed8jebSwQxeOR0xzTzSGi3PYHH+V4toCY2pOf8mHkAiEA8QkiVqZ/BTSM75AsVRULdTcCKfH9+d+3Hz/MFXpH1qI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@borgels%2fmcp-server-saxo@0.2.0-rc.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":949583},"main":"./dist/transports/stdio.js","type":"module","types":"./dist/transports/stdio.d.ts","engines":{"node":">=20.11"},"gitHead":"0e86196dd2bcbab9fb4765a43a9be6bbf1ce2d2c","scripts":{"dev":"tsx src/transports/stdio.ts","auth":"tsx src/bin/auth.ts","test":"vitest run","build":"tsup src/transports/stdio.ts src/transports/http.ts src/bin/auth.ts --format esm --dts --clean --sourcemap","prepack":"npm run typecheck && npm test && npm run build","dev:http":"tsx src/transports/http.ts","mcpb:pack":"mcpb pack . mcp-server-saxo.mcpb","typecheck":"tsc --noEmit","smoke:live":"tsx test/live-smoke.ts","mcpb:validate":"mcpb validate manifest.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f8cf1fa9-e747-40f8-9e5d-157a3f258fc3"}},"overrides":{"hono":">=4.12.16","fast-uri":">=3.1.2","ip-address":">=10.1.1"},"repository":{"url":"git+https://github.com/Borgels/mcp-server-saxo.git","type":"git"},"_npmVersion":"11.15.0","description":"MCP server for the Saxo Bank OpenAPI (SIM and LIVE trading).","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"readmeFilename":"README.md","devDependencies":{"tsx":"^4.21.0","tsup":"^8.5.1","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.7.0","@anthropic-ai/mcpb":"^2.1.2"},"_npmOperationalInternal":{"tmp":"tmp/mcp-server-saxo_0.2.0-rc.1_1779318156557_0.642437802013603","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@borgels/mcp-server-saxo","version":"0.2.0","keywords":["mcp","model-context-protocol","saxo","saxobank","trading","openapi","borgels"],"author":{"name":"Borgels"},"license":"Apache-2.0","_id":"@borgels/mcp-server-saxo@0.2.0","maintainers":[{"name":"abolsen","email":"abo@borgels.com"}],"homepage":"https://github.com/Borgels/mcp-server-saxo#readme","bugs":{"url":"https://github.com/Borgels/mcp-server-saxo/issues"},"bin":{"mcp-server-saxo":"dist/transports/stdio.js"},"dist":{"shasum":"9e5c25744f70f8e4e3a01b6051f4ed2893d32e25","tarball":"https://registry.npmjs.org/@borgels/mcp-server-saxo/-/mcp-server-saxo-0.2.0.tgz","fileCount":20,"integrity":"sha512-iTYHlqKw+bN7tzkMApNVN+VOcmze/CNG+RtijwksXgHll62UPDIdHY4vncY+Gr7LhmxJyrt19ZapMuSYXtTX4w==","signatures":[{"sig":"MEUCIQC/RfQSM41wu4Nb3xJUDvpOYzjrSZS38yJTP6i2c/LBDQIgLQ4S2PVBGCaOg5JcKc4Lsd61SHpy7ElWExtYDPjK0Dk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@borgels%2fmcp-server-saxo@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1273279},"main":"./dist/transports/stdio.js","type":"module","types":"./dist/transports/stdio.d.ts","engines":{"node":">=20.11"},"gitHead":"2d091a48bfb3833ef6d3d7c4363e44556eca40fa","scripts":{"dev":"tsx src/transports/stdio.ts","auth":"tsx src/bin/auth.ts","test":"vitest run","build":"tsup src/transports/stdio.ts src/transports/http.ts src/bin/auth.ts --format esm --dts --clean --sourcemap","prepack":"npm run typecheck && npm test && npm run build","dev:http":"tsx src/transports/http.ts","mcpb:pack":"mcpb pack . mcp-server-saxo.mcpb","typecheck":"tsc --noEmit","smoke:live":"tsx test/live-smoke.ts","mcpb:validate":"mcpb validate manifest.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f8cf1fa9-e747-40f8-9e5d-157a3f258fc3"}},"overrides":{"hono":">=4.12.16","fast-uri":">=3.1.2","ip-address":">=10.1.1"},"repository":{"url":"git+https://github.com/Borgels/mcp-server-saxo.git","type":"git"},"_npmVersion":"11.15.0","description":"MCP server for the Saxo Bank OpenAPI (SIM and LIVE trading).","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.21.0","tsup":"^8.5.1","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.9.1","@anthropic-ai/mcpb":"^2.1.2"},"_npmOperationalInternal":{"tmp":"tmp/mcp-server-saxo_0.2.0_1779384821717_0.20253266240777257","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@borgels/mcp-server-saxo","version":"0.2.1","description":"MCP server for the Saxo Bank OpenAPI (SIM and LIVE trading).","type":"module","main":"./dist/transports/stdio.js","bin":{"mcp-server-saxo":"dist/transports/stdio.js"},"scripts":{"build":"tsup src/transports/stdio.ts src/transports/http.ts src/bin/auth.ts --format esm --dts --clean --sourcemap","dev":"tsx src/transports/stdio.ts","dev:http":"tsx src/transports/http.ts","auth":"tsx src/bin/auth.ts","typecheck":"tsc --noEmit","test":"vitest run","prepack":"npm run typecheck && npm test && npm run build","smoke:live":"tsx test/live-smoke.ts","mcpb:validate":"npx -y -p @anthropic-ai/mcpb@^2.1.2 mcpb validate manifest.json","mcpb:pack":"npx -y -p @anthropic-ai/mcpb@^2.1.2 mcpb pack . mcp-server-saxo.mcpb"},"repository":{"type":"git","url":"git+https://github.com/Borgels/mcp-server-saxo.git"},"keywords":["mcp","model-context-protocol","saxo","saxobank","trading","openapi","borgels"],"author":{"name":"Borgels"},"license":"Apache-2.0","engines":{"node":">=20.11"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"bugs":{"url":"https://github.com/Borgels/mcp-server-saxo/issues"},"homepage":"https://github.com/Borgels/mcp-server-saxo#readme","dependencies":{"@modelcontextprotocol/sdk":"^1.29.0","zod":"^4.4.3"},"overrides":{"ip-address":">=10.1.1","hono":">=4.12.16","fast-uri":">=3.1.2"},"devDependencies":{"@types/node":"^25.9.1","tsup":"^8.5.1","tsx":"^4.21.0","typescript":"^6.0.3","vitest":"^4.1.6"},"gitHead":"dbf96f9e22c629d6b5e817f2174ddcd62810b785","types":"./dist/transports/stdio.d.ts","_id":"@borgels/mcp-server-saxo@0.2.1","_nodeVersion":"22.22.3","_npmVersion":"11.15.0","dist":{"integrity":"sha512-ehpeHpGsmoF2cHVPkcZGvbwg9H3RHuDGztO04Rx9u9JpddG3I19i5CLzvi18HV42+3Y+k0/7qRrrrz89ATwlEQ==","shasum":"a11aea2a6f25b73ced82b1e67e9da6793dffcdf4","tarball":"https://registry.npmjs.org/@borgels/mcp-server-saxo/-/mcp-server-saxo-0.2.1.tgz","fileCount":20,"unpackedSize":1284149,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@borgels%2fmcp-server-saxo@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIE2YP7QjapQL+ravTDJWqepBdIN4/1BZe8+0O1nAHi4MAiEA5rn/llvjq0ieGNF+yO+/I50wthQSTyh9uFcPPzSbDgQ="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f8cf1fa9-e747-40f8-9e5d-157a3f258fc3"}},"directories":{},"maintainers":[{"name":"abolsen","email":"abo@borgels.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mcp-server-saxo_0.2.1_1779386494986_0.014612412233540706"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T19:18:29.798Z","modified":"2026-05-21T18:01:35.513Z","0.1.1":"2026-05-19T19:18:30.139Z","0.2.0-rc.1":"2026-05-20T23:02:36.752Z","0.2.0":"2026-05-21T17:33:42.048Z","0.2.1":"2026-05-21T18:01:35.185Z"},"bugs":{"url":"https://github.com/Borgels/mcp-server-saxo/issues"},"author":{"name":"Borgels"},"license":"Apache-2.0","homepage":"https://github.com/Borgels/mcp-server-saxo#readme","keywords":["mcp","model-context-protocol","saxo","saxobank","trading","openapi","borgels"],"repository":{"type":"git","url":"git+https://github.com/Borgels/mcp-server-saxo.git"},"description":"MCP server for the Saxo Bank OpenAPI (SIM and LIVE trading).","maintainers":[{"name":"abolsen","email":"abo@borgels.com"}],"readme":"# mcp-server-saxo\n\nTypeScript MCP server for the Saxo Bank OpenAPI. Works against both the **SIM**\n(simulation/demo) and **LIVE** environments. Same shape as the rest of the\nBorgels MCP server family: typed, documented, policy-aware, credential-sane,\nand audit-friendly.\n\n> **Disclaimer:** This is an independent, unofficial project by Borgels.\n> Borgels is not affiliated with, endorsed by, or supported by Saxo Bank A/S.\n> \"Saxo\", \"Saxo Bank\", and the Saxo OpenAPI are referenced only to describe\n> what this server talks to. You need your own Saxo developer credentials, and\n> use of the Saxo OpenAPI is subject to Saxo Bank's own terms and licensing.\n> **Trading on LIVE moves real money. You are responsible for any orders this\n> server places on your behalf.**\n\n## Scope\n\nSupported Saxo OpenAPI service groups:\n\n- Root (session, diagnostics)\n- Reference Data (instruments, exchanges)\n- Trading (info prices, snapshot chart, order place / modify / cancel / precheck)\n- Portfolio (accounts, balances, positions, closed positions, orders)\n\nStreaming subscriptions (WebSocket), Value Add (alerts, performance), and\nClient Management beyond `accounts/me` are out of scope for v1.\n\n## Quickstart on SIM\n\n```sh\nnpm install\nnpm run build\ncp .env.example .env\n```\n\nThen pick one of:\n\n### Path A — 24-hour token (quickest, one-shot)\n\n1. Sign up for a free SIM account at <https://www.developer.saxo/>.\n2. **App Management → Generate 24-hour token** (no app required).\n3. Paste it into `.env`:\n   ```\n   SAXO_ENVIRONMENT=sim\n   SAXO_ACCESS_TOKEN=...\n   ```\n4. Start the server (see Path A/B `start` block below).\n\nThe token expires after 24h; you'll need to repeat step 2 to keep going.\n\n### Path B — OAuth app (refreshes automatically)\n\nFor anything you run longer than a day, do the OAuth dance once and let\nthe server refresh tokens for you. This is also the path you'll need for\nLIVE.\n\n1. Sign up at <https://www.developer.saxo/>.\n2. **App Management → Create application** (mark as SIM, allow trading\n   if you'll be placing orders). Pick either grant type — the server\n   supports both:\n   - **Code** (confidential client, has an App Secret)\n   - **PKCE** (public client, no secret — slightly safer for\n     distributable clients but functionally equivalent for this server)\n3. Register the redirect URL in the portal:\n   - **For Code apps**: register exactly `http://localhost:8765/callback`.\n     Saxo matches the full URL including port at runtime, and rejects\n     IP-literal redirects like `http://127.0.0.1:...`, so use the\n     hostname.\n   - **For PKCE apps**: register `http://localhost/callback` — Saxo's\n     PKCE flow requires the registered URL to **omit the port**\n     (\"When registering the redirect URL with your application, it\n     cannot include a port number\"). The server still sends\n     `http://localhost:8765/callback` at runtime; Saxo matches\n     port-blind. Mismatching this returns `unauthorized_client`.\n4. Put the credentials in `.env`:\n   ```\n   SAXO_ENVIRONMENT=sim\n   SAXO_APP_KEY=...\n   SAXO_APP_SECRET=...        # Only for Code-grant apps. Omit for PKCE.\n   SAXO_REDIRECT_URI=http://localhost:8765/callback\n   ```\n5. Run the auth CLI:\n   ```sh\n   npm run auth -- --env sim\n   ```\n   This opens your browser to Saxo's authorize page, you click Allow, and\n   the CLI writes `SAXO_ACCESS_TOKEN`, `SAXO_REFRESH_TOKEN`, and\n   `SAXO_TOKEN_EXPIRES_AT` back into `.env`. Both grant types use the\n   same flow; the server detects the absence of `SAXO_APP_SECRET` and\n   switches to PKCE-style token exchange (no Authorization header,\n   `client_id` in the form body).\n\nThe OAuth access token Saxo issues is short-lived (~20 minutes on SIM),\nbut `SaxoClient` proactively refreshes it from the refresh token ~60s\nbefore expiry, so the server stays alive indefinitely.\n\n### Start the server\n\n```sh\nnpm run dev          # stdio transport\n# or\nnpm run dev:http     # http://127.0.0.1:3000/mcp\n```\n\n### Market data is a second, separate consent\n\nThe 24-hour token unlocks **authenticated API access**, but live bid/ask\nquotes via `/trade/v1/infoprices` require a **separate per-exchange market\ndata agreement** (NYSE, OPRA, EUREX, etc.). Until you accept it,\n`saxo_get_infoprice` returns `PriceTypeAsk/Bid: \"NoAccess\"` with `Amount: 0`,\nand `saxo_session_me` reports `MarketDataViaOpenApiTermsAccepted: false`.\n\nThere is **no Saxo OpenAPI endpoint** to flip this flag programmatically —\nit's a human consent screen. Find it in the Saxo trading platform\n(SaxoTraderGO → settings → live data subscriptions) or developer.saxo. Once\naccepted, the same token starts returning quotes (typically `DelayedByMinutes: 15`\non SIM unless your `DataLevel` is `Realtime`).\n\n`saxo_diagnostics` flags this condition in its `warnings[]` and infoprices\nresponses are decorated with `_warning` when they're NoAccess.\n\n## Authentication\n\nThis server reads credentials from environment variables only — they are\nnever accepted as tool arguments.\n\n| Variable | Required for | Notes |\n| --- | --- | --- |\n| `SAXO_ENVIRONMENT` | always | `sim` (default) or `live` |\n| `SAXO_ACCESS_TOKEN` | always | Bearer token. 24-hour token for SIM, OAuth token for LIVE. |\n| `SAXO_REFRESH_TOKEN` | LIVE / long-running SIM | Together with app credentials enables 401-auto-refresh. |\n| `SAXO_APP_KEY` | refresh / OAuth | Application key from the developer portal. |\n| `SAXO_APP_SECRET` | refresh / OAuth | Application secret. |\n| `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. |\n| `SAXO_TIMEOUT_MS` | optional | Request timeout in ms (default 30000). |\n\n### Two ways to log in for LIVE / long-running SIM\n\n**Option A — CLI (one-off, scriptable):**\n\n```sh\nSAXO_APP_KEY=... SAXO_APP_SECRET=... npm run auth -- --env live\n```\n\nThe CLI starts a local callback listener, opens your browser to the Saxo\nauthorize endpoint with a PKCE challenge, and writes\n`SAXO_ACCESS_TOKEN`/`SAXO_REFRESH_TOKEN`/`SAXO_TOKEN_EXPIRES_AT` back into\n`.env`.\n\n**Option B — From inside the MCP client (`saxo_oauth_*` tools):**\n\n1. Set `SAXO_APP_KEY` + `SAXO_APP_SECRET` in the MCP server environment. In\n   packaged clients such as Claude Desktop / MCPB this usually means client\n   config, not a local env file.\n2. For the smooth local flow, call `saxo_oauth_login`. It starts the loopback\n   listener, opens the browser by default, waits for approval, exchanges the\n   code, and updates the running MCP server in memory.\n3. Tokens are not written to disk unless `writeToEnvFile=true` is supplied.\n   Use `envFilePath` when you want a specific file such as `.env.local`.\n\nFor clients that want to control their own UI, use the lower-level two-step\nflow: call `saxo_oauth_start`, open the returned `authorizeUrl` (or set\n`openBrowser=true`), then call `saxo_oauth_complete` with the returned\n`ticketId`.\n\nThe MCP server only listens on loopback (`127.0.0.1`) for the callback, so the\nflow never touches the public network beyond Saxo itself.\n\n## Install\n\nThe server is published as **`@borgels/mcp-server-saxo`** on npm and\nas an **`.mcpb` bundle** (MCP Bundle, the new name for DXT) on each\n[GitHub Release](https://github.com/Borgels/mcp-server-saxo/releases).\nThe protocol layer is the same everywhere — stdio + MCP JSON-RPC. The\ntable below is just the per-client config syntax.\n\n| Client | How to install |\n| --- | --- |\n| **Claude Desktop (1.8089+)** | Download `mcp-server-saxo-v<version>.mcpb` from the latest [Release](https://github.com/Borgels/mcp-server-saxo/releases), then **Settings → Connectors → Install from file**. Claude Desktop prompts you for the SAXO_* config values from the bundle's user_config schema. |\n| **Claude Desktop (legacy `claude_desktop_config.json`)** | Add the [universal JSON block](#universal-config) to `%APPDATA%\\Claude\\claude_desktop_config.json` under `mcpServers`. Restart fully (File → Exit). |\n| **Claude Code (CLI)** | `claude mcp add saxo -- npx -y @borgels/mcp-server-saxo` (or edit `~/.claude/mcp.json` / per-project `.mcp.json`). |\n| **Cursor** | Settings → MCP → Add server, or `.cursor/mcp.json` per project. Same JSON shape as below. |\n| **Codex (OpenAI)** | `~/.codex/config.toml`: `[mcp_servers.saxo]` with `command = \"npx\"`, `args = [\"-y\", \"@borgels/mcp-server-saxo\"]`, `env = { SAXO_ENVIRONMENT = \"sim\", ... }`. |\n| **Windsurf / Cline / Zed / continue.dev** | Settings UI, point at `npx -y @borgels/mcp-server-saxo`. Same JSON envelope. |\n| **MCP Inspector (debug)** | `npx @modelcontextprotocol/inspector npx -y @borgels/mcp-server-saxo` |\n| **Self-host (any client)** | `command: \"node\", args: [\"/absolute/path/to/dist/transports/stdio.js\"]`. Use double backslashes on Windows or forward slashes. |\n\n### Universal config\n\nFor any client that uses the standard `mcpServers` config schema:\n\n```json\n{\n  \"mcpServers\": {\n    \"saxo\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@borgels/mcp-server-saxo\"],\n      \"env\": {\n        \"SAXO_ENVIRONMENT\": \"sim\",\n        \"SAXO_ACCESS_TOKEN\": \"your-24h-sim-token\"\n      }\n    }\n  }\n}\n```\n\nThat's the **minimal** form — fine for a quick SIM test. For durable\nuse (no daily token paste), do the OAuth dance once with\n`npm run auth -- --env sim` against a clone of this repo, then copy\nthe resulting OAuth env vars into the block:\n\n```json\n{\n  \"mcpServers\": {\n    \"saxo\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@borgels/mcp-server-saxo\"],\n      \"env\": {\n        \"SAXO_ENVIRONMENT\": \"sim\",\n        \"SAXO_APP_KEY\": \"...\",\n        \"SAXO_APP_SECRET\": \"...\",\n        \"SAXO_REFRESH_TOKEN\": \"...\",\n        \"SAXO_ACCESS_TOKEN\": \"...\",\n        \"SAXO_TOKEN_EXPIRES_AT\": \"2026-05-19T18:00:37.823Z\",\n        \"SAXO_ENABLE_LIVE_TRADING\": \"false\"\n      }\n    }\n  }\n}\n```\n\n`SAXO_ACCESS_TOKEN` may be expired at startup — `SaxoClient` decodes\nthe JWT `exp`, detects it's within 60s of expiry, and runs the\nrefresh-token grant before sending the first request. The refresh\ntoken is what really matters at cold start.\n\n### Restart after editing\n\nMost MCP clients spawn server processes only at startup. After editing\nthe client's config, fully quit and reopen it. Some clients (notably\nClaude Desktop) keep running in the system tray when the window is\nclosed — make sure you actually exit before reopening.\n\n### Troubleshooting\n\n- If the server doesn't appear in the client's tool list, check the\n  client's MCP logs (each client documents its log location).\n- Verify `npx -y @borgels/mcp-server-saxo` runs from a fresh shell — it\n  should start, register tools, and wait for stdin without error.\n- Once connected, call `saxo_diagnostics` first when something looks\n  off — its `warnings[]` array surfaces missing market-data terms,\n  near-expiry tokens, `DataLevel` not Realtime, and live env without\n  `SAXO_ENABLE_LIVE_TRADING`.\n\n### Building from source (for hacking or before npm publish)\n\n```sh\ngit clone https://github.com/Borgels/mcp-server-saxo.git\ncd mcp-server-saxo\nnpm install\nnpm run build           # produces dist/transports/stdio.js\nnpm test\n```\n\nUse `\"command\": \"node\", \"args\": [\"/absolute/path/to/dist/transports/stdio.js\"]`\nin your client config to point at the built source. For the MCPB bundle,\n`npm run mcpb:pack` produces `mcp-server-saxo.mcpb` in the project root.\n\n## Start Here\n\nUse `saxo_capabilities` first when an MCP client needs to decide which Saxo\ntool to call. It returns tool descriptions, examples, identifier formats, and\nsafety notes without contacting Saxo.\n\n```json\n{ \"query\": \"place order\", \"limit\": 5 }\n```\n\n### Optional Alpha Vantage enrichment\n\nThe strategy screeners are Saxo-first. They work without third-party data using\nSaxo instruments, prices, chart bars, option chains, and account/position\ncontext. If `ALPHA_VANTAGE_API_KEY` is set, stock and option strategy tools can\noptionally enrich candidates with Alpha Vantage `OVERVIEW`,\n`NEWS_SENTIMENT`, and `EARNINGS_CALENDAR`.\n\nLeave `ALPHA_VANTAGE_API_KEY` unset to keep the server Saxo-only. Alpha\nVantage is optional because tiers and rate limits vary. For deeper research,\nrun Alpha Vantage's own MCP server beside this Saxo server and pass normalized\nresearch into `externalContextBySymbol`.\n\n### Strategy and portfolio planning\n\nUse `saxo_screen_stock_strategies` when you want ranked stock ideas with\naccount-aware sizing, Saxo quote/liquidity data, Saxo chart context, optional\nfundamentals/news enrichment, risks, and decision briefs:\n\n```json\n{\n  \"accountKey\": \"your-account-key\",\n  \"market\": \"us\",\n  \"universe\": \"large_cap\",\n  \"objective\": \"balanced\",\n  \"riskProfile\": \"balanced\",\n  \"includeAccountContext\": true,\n  \"includeFundamentalContext\": true,\n  \"riskBudgetPercentPerIdea\": 1,\n  \"maxSingleNamePercent\": 10,\n  \"maxResults\": 10\n}\n```\n\nUse `saxo_screen_option_strategies` when you want the server to find\noptionable underlyings first and then rank account-aware option strategy plans:\n\n```json\n{\n  \"accountKey\": \"your-account-key\",\n  \"market\": \"us_nasdaq\",\n  \"underlyingUniverse\": \"auto\",\n  \"playbook\": \"income_30_60d\",\n  \"riskProfile\": \"balanced\",\n  \"includeAccountContext\": true,\n  \"riskBudgetPercent\": 1,\n  \"requireGreeks\": true,\n  \"maxThetaDailyPercentOfRisk\": 1,\n  \"maxUnderlyings\": 50,\n  \"maxUnderlyingScan\": 500,\n  \"maxSymbolsToPlan\": 5,\n  \"maxPlans\": 10\n}\n```\n\nOption strategy planning uses Saxo option-chain quotes, Saxo Greeks, optional\nOptionsChain IV context, and account-aware sizing. Multi-leg candidates include\naggregated net delta, gamma, theta, and vega; long-premium structures such as\nlong calls and debit spreads are penalized when theta decay is large relative\nto max risk. Set `requireGreeks=true` to reject candidates when Saxo does not\nreturn complete delta, gamma, theta, and vega, and use\n`maxThetaDailyPercentOfRisk` to cap acceptable daily decay.\n\nUse `saxo_plan_portfolio_strategy` for whole-account deployment. It reads the\naccount snapshot, runs the stock and option screeners, then returns target\nallocation, staged deployment, stock allocation, option satellite candidates,\nsector exposure, and portfolio-level risk warnings. For generic options\nportfolio planning, pass `optionTheses` to describe each options sleeve; stock\nideas remain first-class through `stockSymbols`, `includeStocks`, stock\nallocation caps, and the stock strategy screener. The planner uses guardrailed\nsizing by default and requires explicit input for concentrated conviction risk:\n\n```json\n{\n  \"accountKey\": \"your-account-key\",\n  \"objective\": \"balanced_growth_income\",\n  \"riskProfile\": \"balanced\",\n  \"portfolioProfile\": \"concentrated_conviction\",\n  \"deploymentStyle\": \"staged\",\n  \"targetInvestedPercent\": 80,\n  \"cashReservePercent\": 10,\n  \"maxCashDollars\": 1000,\n  \"maxSingleNamePercent\": 10,\n  \"maxSectorPercent\": 35,\n  \"maxOptionsRiskPercent\": 5,\n  \"riskBudgetPercentPerIdea\": 1,\n  \"maxSelectedUnderlyings\": 5,\n  \"minPositionRiskDollars\": 2500,\n  \"maxContractsPerPosition\": 20,\n  \"fragmentationPolicy\": \"reject\",\n  \"requireGreeks\": true,\n  \"maxThetaDailyPercentOfRisk\": 1,\n  \"stockUniverse\": \"large_cap\",\n  \"stockMaxCandidates\": 120,\n  \"discoverOptionCandidates\": true,\n  \"optionDiscoveryUniverse\": \"auto\",\n  \"optionDiscoveryPlaybook\": \"long_term_directional\",\n  \"optionTheses\": [\n    {\n      \"name\": \"Long-term stock replacement\",\n      \"symbols\": [\"NVO\"],\n      \"role\": \"core_conviction\",\n      \"conviction\": \"high\",\n      \"horizon\": \"leaps\",\n      \"preferredStructures\": [\"long_call\", \"debit_spread\"],\n      \"targetRiskPercent\": 5\n    }\n  ],\n  \"includeStocks\": true,\n  \"includeOptions\": true\n}\n```\n\nPortfolio planning is read-only. It never calls precheck or places orders.\nStock allocation de-duplicates issuer/share-class duplicates such as\n`GOOG`/`GOOGL`. `maxSectorPercent` is enforced when sector data is available\nfrom fundamentals context; otherwise the response includes a warning instead\nof pretending sector caps were applied. Stock candidate discovery is controlled\nwith `stockUniverse`, `stockMarket`, `stockMaxCandidates`, and\n`stockMaxTechnicalCandidates`; if explicit `stockSymbols` are not supplied, the\nstock screener can still build a ranked universe from Saxo data. Option thesis\noutput includes selected and rejected candidates, thesis budgets, max-loss\nexposure, expiry buckets, strategy mix, deployment rules, and simple scenario\nnotes. Selected option candidates also include `entryTiming`, which classifies\ncurrent price action as `enter`, `scale_in`, `wait`, or `avoid` using the\nunderlying price, breakeven, SMA20/SMA50 distance, 5d/20d returns, and recent\naverage range. This lets the planner distinguish a buyable pullback from a\ntechnical breakdown before sizing tranches.\nFor an options-only account, set `includeStocks=false`; the planner then keeps\nstock sleeves at zero and treats the options risk budget as the deployable\naccount sleeve instead of a small satellite. Use `maxCashDollars` when the cash\nreserve is a hard dollar cap rather than only a percentage. With\n`deploymentStyle=\"immediate\"`, options-only contract counts scale up to the\nconfigured thesis, trade, and cash-reserve budgets; staged plans remain\nstarter-sized. The risk dashboard reports `deployableCashDollars`,\n`cashReserveDollars`, `unallocatedOptionBudget`, and warns when a strict\noptions-only plan leaves material cash undeployed because no additional\nGreek-backed candidates fit the current rules.\nSet `discoverOptionCandidates=true` to blend user-supplied conviction theses\nwith a deterministic discovery sleeve from Saxo's market mover and option\nscreeners. Discovery candidates are still filtered by liquidity, Greeks, theta,\naccount sizing, and the same portfolio risk caps.\nUse `portfolioProfile=\"concentrated_conviction\"` plus\n`maxSelectedUnderlyings`, `minPositionRiskDollars`, `maxContractsPerPosition`,\nand `fragmentationPolicy=\"reject\"` when the account should favor fewer,\nlarger, easier-to-monitor option positions instead of many small trades.\nIf the Saxo account is not approved for short option legs, set\n`allowShortOptionLegs=false` on option strategy or portfolio planning calls.\nThe planner then filters out spreads and short-premium structures that open a\nshort option leg and only returns long-only option structures that pass the\nremaining liquidity, Greeks, and theta gates.\n\nUse `saxo_review_strategy_positions` after execution to monitor open stock and\noption strategies against the plan you opened. Pass the executed stock leg or\noption legs plus entry metrics from the selected plan or fill. The tool matches\nthose legs to open Saxo positions, refreshes quotes, estimates current value\nand P/L, and evaluates deterministic profit-taking/loss rules. Option\nstrategies additionally include Greeks, theta, DTE, roll, and close rules. It\nreturns verdicts such as `hold`, `review`, `consider_trim`, `consider_close`,\nand `roll_watch`; execution remains a separate explicit order workflow.\n\n## Tools\n\nAll tools are registered with MCP annotations (`readOnlyHint`,\n`destructiveHint`, `idempotentHint`, `openWorldHint`) so clients can reason\nabout safety. Read-only tools work on SIM and LIVE without extra opt-in. Write\ntools (orders, OAuth) follow the [LIVE Trading Safety](#live-trading-safety)\nrules.\n\n### Read-only\n\n| Tool | Endpoint | Purpose |\n| --- | --- | --- |\n| `saxo_capabilities` | — | Discover tools without calling Saxo. |\n| `saxo_session_me` | `GET /port/v1/users/me` | Authenticated user (Name, ClientKey, UserKey, MarketDataViaOpenApiTermsAccepted). |\n| `saxo_diagnostics` | (aggregated) | Session + capabilities + token expiry + warnings (market-data terms, DataLevel, token close to expiry). |\n| `saxo_feature_availability` | `GET /root/v1/features/availability` | Inspect Saxo feature flags for News, Calendar, Gainers/Losers, and Chart. |\n| `saxo_search_instruments` | `GET /ref/v1/instruments` | Search by keyword + asset type. |\n| `saxo_get_instrument_details` | `GET /ref/v1/instruments/details` | Detailed metadata for one or many Uics. |\n| `saxo_list_exchanges` | `GET /ref/v1/exchanges` | List exchanges (or one by ExchangeId). |\n| `saxo_get_option_chain` | `GET /ref/v1/instruments/contractoptionspaces/{optionRootId}` | Strikes + expirations. `normalize=true` (default) pivots Put/Call into one row per strike. |\n| `saxo_list_option_expiries` | (uses option chain) | Cheap helper: just the expiries (date, days, strike count) for an option root. |\n| `saxo_list_standard_option_expiries` | `GET /ref/v1/standarddates/optionexpiry` | Standardized option-expiry calendar (3rd Friday monthlies, quarterlies, weeklies). Distinct from `list_option_expiries`. |\n| `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. |\n| `saxo_get_infoprice` | `GET /trade/v1/infoprices` | Snapshot bid/ask/last for one instrument. Adds `_warning` if `PriceType=NoAccess`. |\n| `saxo_get_infoprices_list` | `GET /trade/v1/infoprices/list` | Snapshot prices for multiple Uics. |\n| `saxo_get_chart` | `GET /chart/v3/charts` | Historical OHLC bars (horizon in minutes). |\n| `saxo_screen_market` | Saxo instruments + info prices | User-friendly top gainers/losers and pre-market screeners. |\n| `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. |\n| `saxo_estimate_vertical_spread` | (pure math) | Given side + strikes + debit + contracts: max loss, max gain, breakeven, R/R. Applies 100x option multiplier. |\n| `saxo_plan_option_strategy` | Option chain + prices | Opinionated read-only option strategy plans. |\n| `saxo_screen_option_strategies` | Market screener + chart TA + OptionsChain IV + planner | Cross-symbol options strategy screening. |\n| `saxo_screen_stock_strategies` | Saxo instruments + prices + chart TA + optional fundamentals/news | Opinionated stock strategy screening with decision briefs. |\n| `saxo_plan_portfolio_strategy` | Account snapshot + stock/options screeners | Whole-account target allocation, staged deployment, and risk dashboard. |\n| `saxo_review_strategy_positions` | Positions + quotes (+ Greeks for options) | Post-execution strategy follow-up with hold/trim/close/roll verdicts. |\n| `saxo_list_accounts` | `GET /port/v1/accounts/me` | List the client's trading accounts. |\n| `saxo_get_balance` | `GET /port/v1/balances` | Cash + margin balance. |\n| `saxo_list_positions` | `GET /port/v1/positions/me` | Open positions (one row per fill). |\n| `saxo_list_net_positions` | `GET /port/v1/netpositions/me` | Positions aggregated per instrument (one row per Uic). Right view for current exposure. |\n| `saxo_list_closed_positions` | `GET /port/v1/closedpositions/me` | Closed positions / history. |\n| `saxo_list_activities` | `GET /port/v1/activities` | Recent account events: orders placed/modified/cancelled, trades, dividends, corporate actions. |\n| `saxo_list_orders` | `GET /port/v1/orders/me` | Working orders. |\n| `saxo_get_order` | `GET /port/v1/orders/{orderId}` | One order by id. |\n\n### Write — guarded\n\n| Tool | Endpoint | Guards |\n| --- | --- | --- |\n| `saxo_precheck_order` | `POST /trade/v2/orders/precheck` | Policy + audit. No execution. |\n| `saxo_place_order` | `POST /trade/v2/orders` | LIVE: `SAXO_ENABLE_LIVE_TRADING=true` + `policy.json` allow + optional auto-precheck. |\n| `saxo_modify_order` | `PATCH /trade/v2/orders` | Same as place_order. |\n| `saxo_cancel_order` | `DELETE /trade/v2/orders/{ids}` | Policy + audit. |\n| `saxo_precheck_multileg_order` | `POST /trade/v2/orders/multileg/precheck` | Validate a spread (no execution). |\n| `saxo_place_multileg_order` | `POST /trade/v2/orders/multileg` | Place a spread atomically with a single net debit/credit limit. |\n| `saxo_modify_multileg_order` | `PATCH /trade/v2/orders/multileg` | Adjust spread Amount or OrderPrice. |\n| `saxo_cancel_multileg_order` | `DELETE /trade/v2/orders/multileg/{id}` | Cancel the whole strategy. |\n| `saxo_oauth_login` | OAuth2 PKCE | One-call local login; updates in-process tokens. Optional env-file persist. |\n| `saxo_oauth_start` | OAuth2 PKCE | Loopback redirect only; reads app creds from env. |\n| `saxo_oauth_complete` | OAuth2 PKCE | Replaces in-process tokens. Optional `.env` persist. |\n| `saxo_oauth_cancel` | — | Closes a pending OAuth listener. |\n\n### Place / modify order body\n\nOrder tools accept Saxo's standard `POST /trade/v2/orders` body. Required\nfields: `AccountKey`, `Uic`, `AssetType`, `BuySell`, `Amount`, `OrderType`,\n`OrderDuration`. Optional: `OrderPrice` (Limit/StopLimit), `StopPrice`\n(Stop/StopLimit), `ManualOrder`, `ExternalReference`, and `Orders[]` for\nrelated orders (OCO, IfDone, brackets).\n\n```json\n{\n  \"AccountKey\": \"your-account-key\",\n  \"Uic\": 211,\n  \"AssetType\": \"Stock\",\n  \"BuySell\": \"Buy\",\n  \"Amount\": 1,\n  \"OrderType\": \"Market\",\n  \"OrderDuration\": { \"DurationType\": \"DayOrder\" }\n}\n```\n\n### Multi-leg option order body\n\nMulti-leg tools wrap Saxo's `/trade/v2/orders/multileg` family. `OrderType`\nmust be `Limit`. `OrderPrice` is always positive — the absolute limit\nprice you are willing to pay (debit spread) or receive (credit spread).\nSaxo's API rejects negative `OrderPrice` with \"Price cannot be\nnegative\"; the debit/credit direction is implicit in each leg's\n`BuySell`. `Legs[]` accepts 2–20 legs that all share the same option\nroot (same underlying + expiry).\n\n```json\n{\n  \"AccountKey\": \"your-account-key\",\n  \"OrderType\": \"Limit\",\n  \"OrderPrice\": 1.08,\n  \"OrderDuration\": { \"DurationType\": \"GoodTillCancel\" },\n  \"ManualOrder\": true,\n  \"ExternalReference\": \"bull-call-spread-1\",\n  \"Legs\": [\n    {\n      \"Uic\": 14853018,\n      \"AssetType\": \"StockOption\",\n      \"BuySell\": \"Buy\",\n      \"Amount\": 150,\n      \"ToOpenClose\": \"ToOpen\"\n    },\n    {\n      \"Uic\": 14853056,\n      \"AssetType\": \"StockOption\",\n      \"BuySell\": \"Sell\",\n      \"Amount\": 150,\n      \"ToOpenClose\": \"ToOpen\"\n    }\n  ]\n}\n```\n\nSaxo returns a `MultiLegOrderId` plus per-leg `Orders[].OrderId` values.\nUse `saxo_modify_multileg_order` (Amount/OrderPrice only) or\n`saxo_cancel_multileg_order` (cancels the whole strategy) afterwards. To\nfind the per-leg Uics, start with `saxo_search_instruments` for the\nunderlying, then `saxo_get_option_chain` to read off strikes and\nexpirations.\n\n## LIVE Trading Safety\n\nLIVE writes are denied by default. To enable them you must do **all three**:\n\n1. Set `SAXO_ENVIRONMENT=live`.\n2. Set `SAXO_ENABLE_LIVE_TRADING=true`.\n3. Point `SAXO_POLICY_PATH` at a `policy.json` that sets\n   `\"allow_live_writes\": true`.\n\nA copy of `policy.example.json` is included. Supported fields:\n\n| Field | Effect |\n| --- | --- |\n| `allow_live_writes` | Master switch for all order writes on LIVE. |\n| `require_precheck_on_live` | Place-order automatically runs precheck first. |\n| `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. |\n| `allowed_asset_types` | Whitelist of AssetTypes that may be ordered. |\n| `allowed_account_keys` | Whitelist of AccountKeys that may be traded. |\n| `denied_uics` | Blocklist of Uics. |\n| `max_order_amount` | Per-AssetType caps; `default` falls back when no specific entry. |\n| `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 `|OrderPrice| * largest leg Amount * multiplier`. |\n\nEven on SIM, all write tools run through the policy check (it just defaults\nto permissive). Use the policy in SIM too if you want predictable limits.\n\n## Optional HTTP Server\n\nThe local stdio transport is the default for agent compatibility. A small\nStreamable HTTP entry point is also available:\n\n```sh\nPORT=3000 SAXO_ACCESS_TOKEN=... npm run dev:http\n```\n\nBy default the HTTP server binds to `127.0.0.1`, limits request bodies to\n10 MiB, allows browser CORS only from loopback origins, and does not require\nan HTTP Bearer token. Override with `MCP_HTTP_HOST`, `MCP_MAX_BODY_BYTES`,\n`MCP_ALLOWED_ORIGINS`, `MCP_ALLOW_ANY_ORIGIN=true`, and `MCP_HTTP_TOKEN`. The\nMCP endpoint is `POST http://127.0.0.1:3000/mcp`.\n\n## Verification\n\n```sh\nnpm run typecheck\nnpm test\nnpm run build\n```\n\nOptional live SIM smoke test (requires a 24-hour SIM token):\n\n```sh\nSAXO_ACCESS_TOKEN=\"your-sim-token\" npm run smoke:live\n```\n\n## Releasing\n\nTagged releases trigger\n[`.github/workflows/release.yml`](.github/workflows/release.yml). On\n`git push --tags vX.Y.Z`:\n\n1. Typecheck, test, build, validate the MCPB manifest.\n2. Publish `@borgels/mcp-server-saxo@X.Y.Z` to npm with `--provenance`\n   so the package carries a SLSA attestation linking it to the exact\n   CI run + commit.\n3. Reinstall with `--omit=dev` and pack a small `.mcpb` bundle\n   (~2.6 MB; full dev install yields ~17 MB).\n4. Create a GitHub Release with the matching CHANGELOG section as the\n   body and the `.mcpb` attached.\n\nPre-release tags (e.g. `v0.1.1-rc.1`) publish under the `next` npm\ndist-tag and as a GitHub pre-release.\n\n### One-time setup before the first release\n\n1. **Create the npm org.** At\n   <https://www.npmjs.com/org/create>, create the `borgels`\n   organization (free, public packages).\n2. **Bootstrap the first publish.** Trusted Publishing (see below)\n   can't be configured until the package exists on npm, so the very\n   first publish needs a token. Either:\n   - **Local one-shot:** `npm publish --access public` with a one-time\n     Automation Token logged in as a member of the `borgels` org. Run\n     this once from a clean checkout of the tagged commit. Revoke the\n     token immediately afterwards.\n   - **CI bootstrap:** add the token as the `NPM_TOKEN` repo secret,\n     push `v0.1.1-rc.1`, let CI publish, then delete the secret.\n3. **Configure Trusted Publishing** (one-time, per package):\n   <https://www.npmjs.com/package/@borgels/mcp-server-saxo/access> →\n   *Trusted publishing* → Add publisher. Enter\n   - Organization: `Borgels`\n   - Repository: `mcp-server-saxo`\n   - Workflow filename: `release.yml`\n   - Environment: leave blank (or set to e.g. `production` if you\n     want to gate releases behind a GitHub Environment approval).\n4. **Revoke the bootstrap token** and delete the `NPM_TOKEN` repo\n   secret. From this point on, every release authenticates via\n   GitHub Actions OIDC — no long-lived secrets anywhere.\n\nThe same three steps repeat once per sibling Borgels MCP server when\nthey migrate to this template.\n\n## Rate Limits\n\nSaxo applies per-service-group rate limits (typically ~120 requests per\nminute per session per service group, and ~1 order per second). On `429` the\nserver preserves the `retry-after` header on the thrown `SaxoHttpError` so\ncallers can back off.\n\n## Security And Audit\n\n- All credentials (`SAXO_ACCESS_TOKEN`, `SAXO_REFRESH_TOKEN`, `SAXO_APP_KEY`,\n  `SAXO_APP_SECRET`) are read only from the MCP server environment.\n- Credentials are never accepted as tool arguments.\n- Error formatting and audit records redact `Authorization: Bearer ...`,\n  `access_token`, `refresh_token`, and `SAXO_APP_SECRET` style material.\n- The OAuth listener only binds to loopback. Non-loopback `SAXO_REDIRECT_URI`\n  is rejected at startup.\n- If `SAXO_AUDIT_LOG` is set, every tool call writes a JSONL line with\n  timestamp, request id, tool name, environment, action (`start` / `finish`\n  / `error` / `policy_denied`), SHA-256 hash of the input, status, and\n  redacted error text. Raw inputs and tokens are not written.\n- Reports of suspected vulnerabilities go privately to\n  <security@borgels.com>. Do not include credentials or personal data in\n  public GitHub issues.\n\n## API Sources\n\n- Saxo Developer Portal: <https://www.developer.saxo/>\n- Saxo OpenAPI Reference: <https://www.developer.saxo/openapi/referencedocs>\n- Saxo OpenAPI Learn: <https://www.developer.saxo/openapi/learn>\n\n## License\n\nApache-2.0. See [LICENSE](LICENSE).\n","readmeFilename":"README.md"}