Agent Gateway

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 sourceAccepted?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.

Why the registration endpoint is open. Claude.ai and ChatGPT's connector backends self-register a client the first time a user adds the connector — they cannot complete a manual client-registration step ahead of time, so the registration endpoint has to accept anonymous requests. Registering a client only gets it a 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

ClientSetup
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.