Agent Gateway
The Agent Gateway (arb_bot/agent_gateway/) is a standalone FastAPI
service, fully isolated from live trading, that exposes the portfolio and
options-research data as an MCP (Model Context Protocol) server for remote
clients such as Claude Desktop, ChatGPT, and Hermes. It serves
https://agents.portfolioplanner.online/mcp (Streamable HTTP,
SSE-streamed responses) plus a small advisory answer API.
The gateway exposes 24 MCP tools — agent_ask (a coordinated
question-answering entry point) plus 23 read-only portfolio/options/research
tools (holdings, transactions, income, goals, watchlist, corporate actions,
connections, liabilities, SIP plans, mandates, computed reports, options
positions/performance/activity, Options Council decisions, Decision Battle,
decision-observability traces, AI research dossiers, thesis drift). Two
further advisory capabilities (portfolio_start_ai_research,
options_run_council) are registered for scope enforcement but
served over the REST answer API's POST /api/agent/v1/runs
endpoint rather than as MCP tools; they can start a research or Council
run but never place,
modify, or cancel an order. The central tool registry
(arb_bot/agent_gateway/registry.py) refuses to register any
tool whose required scope starts with live:,
broker:token:, risk:override, or
mode:live — the gateway has no broker, order, or execution
authority by construction, and any Council/AI-research run it triggers
stays OBSERVE_ONLY.
Authentication
The gateway validates Keycloak-issued RS256 access tokens only.
Every request to /mcp (and the REST answer API) must carry a
bearer access token minted by the agent-gateway realm at
https://auth.portfolioplanner.online/realms/agent-gateway
(AGENT_OAUTH_ISSUER). Validation is audience-bound
(AGENT_OAUTH_AUDIENCE, defaulting to
${AGENT_PUBLIC_BASE_URL}/mcp), JWKS-fetched
(AGENT_OAUTH_JWKS_URI), and pinned to RS256 only
(AGENT_OAUTH_ALLOWED_ALGORITHMS=RS256) — HMAC and
none are rejected. The bearer is also gated by an explicit
allowlist of trusted client ids (AGENT_OAUTH_ALLOWED_CLIENT_IDS)
and, where a dashboard user owns the linked subject, by an
UnmappedOAuthSubject check at middleware (see
MCP & Google connection).
The gateway never accepts tokens minted by an external IdP
directly. Federated logins — including the Google-brokered path
used by the dashboard’s self-service linking flow — must still produce
a Keycloak-issued access token in the local realm. Google identity
tokens, raw user-bearer tokens forwarded by clients, dashboard session
cookies, and any pre-shared static secret are all rejected by
construction. MCP clients only need the URL
(https://agents.portfolioplanner.online/mcp) — the OAuth
flow (Dynamic Client Registration + Authorization Code + PKCE against
Keycloak) is what binds an access token to a person and that person’s
dashboard-owned Keycloak subject.
| Trust source | Accepted? | Notes |
|---|---|---|
Keycloak realm agent-gateway access token (RS256) |
Yes — the only accepted bearer source | Issuer / audience / signature / expiry / client-id allowlist
enforced on every request. iat future-skew is
rejected; the cached JWKS continues to validate unexpired tokens
when Keycloak is temporarily unreachable; cold-cache fetch
failures are fail-closed (401). |
| Google-issued access or identity token | No | Google is only an IdP broker; Keycloak is the local issuer. A federated login mints a Keycloak access token as the final artifact. |
Dashboard session cookie |
No | The gateway is a separate process; the FastAPI cookie authenticator is not wired to the MCP middleware. |
| Static / pre-shared bearer secret | No | The old AGENT_BEARER_TOKEN fallback path is
removed; no AGENT_BEARER_TOKEN environment variable
is read by the gateway. The bundled stdio proxy
(agent_gateway_proxy.py) can still hand a static
token to clients, but it must mint a Keycloak access
token first (the proxy talks to Keycloak’s token endpoint
under the hood, never to the gateway directly). |
The OAuth identity provider is a self-hosted Keycloak instance at
auth.portfolioplanner.online (realm agent-gateway),
run as an always-on production service in docker-compose.yml —
not a dev-mode instance. scripts/keycloak_bootstrap.sh
idempotently creates the realm, attaches an audience-mapped client scope,
provisions the dashboard-mcp-link confidential client used by
the dashboard’s self-service Google / MCP link flow, and, when all three linking bootstrap secrets are supplied, reconciles
the Google IdP and dashboard client. The dashboard client enforces PKCE S256,
disables token exchange, and uses a client-session-note protocol mapper to
copy Keycloak’s identity_provider note into the ID-token
broker claim. When all three linking secrets are absent those
resources are skipped; a partial linking configuration fails immediately. See
MCP & Google connection for the
user-facing link flow and
docs/superpowers/specs/2026-07-22-agent-gateway-deployment-runbook.md
for the operator-facing bootstrap / rollback runbook.
client_id; it
does not grant access to any data. Every session still requires a real
Authorization Code + PKCE login (a human authenticating against
Keycloak), and every access token is still validated against issuer,
audience, signature, and expiry on every request.
Connecting a client
| Client | Setup |
|---|---|
| Claude Desktop | Add a custom connector with URL
https://agents.portfolioplanner.online/mcp. Claude
Desktop triggers a one-time browser OAuth login (DCR
self-registration + Authorization Code + PKCE) the first time it
connects. |
| ChatGPT | Same URL, added via ChatGPT's connector settings; the same one-time browser OAuth login flow. |
Hermes (self-hosted, hermes.portfolioplanner.online) |
Hermes is self-hosted software the user configures directly, so
use the same Keycloak Authorization Code + PKCE flow against
https://auth.portfolioplanner.online/realms/agent-gateway.
No static-bearer or custom-header shortcut is supported — the
gateway accepts only Keycloak access tokens. |
See docs/superpowers/specs/2026-07-22-agent-gateway-deployment-runbook.md
for the operator-facing deploy/rollback runbook and
docs/superpowers/specs/2026-07-22-agent-gateway-mcp-oauth-design.md
for the full OAuth+DCR design.
Tax-lot report limits
portfolio_get_report(report_type="tax_lots") uses FIFO lots through the requested financial-year end. Listed-equity treatment uses sale-date rates and authoritative transaction provenance: short-term equity treatment requires authoritative transfer-side STT, while long-term equity shares also require acquisition eligibility or a recorded statutory exception. Equity-oriented fund and business-trust units require transfer-side evidence. Generic mutual funds and debt labels (for example Bond or Fixed Income) remain unresolved rather than receiving a guessed tax rate.
The report applies permitted capital-loss set-off (short-term losses against capital gains; long-term losses against long-term gains), then applies the section 112A aggregate exemption: ₹100,000 before FY 2024-25 and ₹125,000 from FY 2024-25. Unknown provenance, unclassified instruments, unmatched sells, or missing grandfathering FMV produce PARTIAL and exempt: null,taxable: null, and est_tax: null rather than a fabricated estimate. Theltcg and stcg fields remain reconstructed accounting totals, not final taxable totals.