Skip to content

Releases: cgbarlow/iris

v6.0.15 — decorate create_* tool responses with web_url (ADR-175)

13 May 22:53
4d39dd7

Choose a tag to compare

Fixed

create_* MCP tool responses now include web_url so the model can link the user straight to the new entity (ADR-175).

Pre-v6.0.15, every read tool was decorated with web_url via links.with_web_url(...), but the create_* tools returned a bare entity dict. The model had no link to surface and had to guess the host. Concrete v6.0.14 failure:

User: link me to it
Claude: https://iris.chrisbarlow.nz/sets/df7aa9df-...   ← wrong host

v6.0.15 wraps the return of _create_collection, _create_set, _create_package, and _create_diagram with with_web_url(result.model_dump_json(), "<kind>"). The model now surfaces the real frontend URL.

Out of scope

apply_diagram_creation has a batch response ({diagram_ids: [str], primary_diagram_id: str}) — bare id strings need a different decoration shape (e.g. web_urls: list[str]). Deferred to a follow-up.

Tests

5 new in test_create_tools_web_url_decoration.py. 173/173 MCP tests pass.

Deploy

  • iris-mcp auto-deploys from main.
  • No migrations.
  • No env-var changes.

See also

v6.0.14 — accept iris-OAuth JWTs in Supabase mode (issue #119)

13 May 20:22
88453f0

Choose a tag to compare

Fixed

iris-OAuth-issued JWTs now validate in Supabase deployment mode (ADR-174).

v6.0.13 got the connector to "Connected" — token exchange returned 200 — but the very first authenticated API call got 401, and every subsequent write too. Live iris-api logs pinpointed it: in Supabase mode, _get_current_user_supabase validates JWTs with the Supabase signing key. iris-OAuth tokens are signed with the iris JWT secret. Different keys → signature always fails → 401.

The bug existed from v6.0.0 but was hidden because the existing OAuth tests run in SQLite mode where _get_current_user_sqlite validates with the iris JWT secret.

Fix: hybrid validation. JWTs with aud="iris-mcp" (canonical OAuth audience) route through the iris HS256 validator using config.auth.jwt_secret. Everything else stays on the existing Supabase validation path. Per-issuer signature validation — no fall-through, so a token claiming aud="iris-mcp" with the wrong signature stays 401'd.

Security: IRIS_JWT_SECRET now required in production

The dev default in config.py is a hardcoded string in the public repo — anyone could forge OAuth-issued JWTs against any deployment that hasn't overridden it. v6.0.14 adds IRIS_JWT_SECRET to render.yaml with sync: false; operator generates with openssl rand -hex 32 and pastes into the Render dashboard for the iris-api service.

Render Blueprint sync does NOT auto-apply env-var additions to existing services — same gotcha as IRIS_MCP_PUBLIC_URL/IRIS_WEB_URL. Operator action required after deploy.

Operator action required

  1. Set IRIS_JWT_SECRET on the iris-api service in the Render dashboard (Environment → Add Environment Variable → value: openssl rand -hex 32 output).
  2. Disconnect + reconnect Iris connector in claude.ai (old bearers signed with the dev-default secret won't validate against the new secret).

Then: Sign in → Allow → Connected → write tools succeed.

Tests

3 new in test_auth/test_supabase_mode_oauth_token.py. 120/120 OAuth + auth tests pass.

See also

v6.0.13 — fix OAuth token-exchange Postgres bool-vs-int crash (issue #119)

13 May 20:06
2e1c223

Choose a tag to compare

Fixed

OAuth token exchange crashed on Postgres with a SQLite/Postgres bool-vs-int type mismatch (ADR-173).

The connector dance reached the final step in v6.0.12 — user signed in, tapped Allow, was redirected back with an auth code — and the token exchange at POST /oauth/token blew up with:

asyncpg.exceptions.DatatypeMismatchError:
  column "revoked" is of type boolean but expression is of type integer

claude.ai surfaced this as mcp_token_exchange_failed. Live Render logs pinpointed oauth/service.py:260 in create_refresh_token.

Root cause: oauth_refresh_tokens.revoked is BOOLEAN on Postgres (Supabase) but INTEGER on SQLite. Three call sites used bare-int SQL literals (VALUES (..., 0), SET revoked = 1). SQLite accepted both; Postgres is strict. The existing 40 OAuth tests all passed against SQLite, so the bug shipped from v6.0.0 (when OAuth shipped) until live production testing surfaced it.

Fix: parameterise as Python bool so the DB adapter coerces to the right SQL type on either backend.

Added

  • Static regression guard (test_postgres_bool_int_compatibility.py, 4 cases) scans the OAuth service source for bare-int-on-bool antipatterns. Catches future drift on SQLite-only CI without needing a Postgres test fixture.

Tests

44/44 OAuth tests pass (40 existing + 4 new).

Deploy

  • iris-api auto-deploys on push to main.
  • No migrations.
  • No env-var changes.

User-visible

  • claude.ai mobile → Sign in on Iris connector → consent page → Allow → connector status goes to Connected (no more mcp_token_exchange_failed).
  • Write tools succeed with the issued bearer.

See also

v6.0.12 — derive OAuth frontend URL from CORS_ORIGINS as fallback (issue #119)

13 May 19:10
4cc8efb

Choose a tag to compare

Fixed

OAuth authorization_endpoint now derives from IRIS_CORS_ORIGINS when IRIS_WEB_URL isn't set (ADR-172).

v6.0.11 wired the endpoint through IRIS_WEB_URL and added the env var to render.yaml. Live iris-api still served the API host as authorization_endpoint post-deploy because Render's Blueprint-sync doesn't auto-apply env-var additions to existing services (same gotcha as IRIS_MCP_PUBLIC_URL on iris-mcp in v6.0.9).

v6.0.12 makes the code robust to env-var drift: if IRIS_WEB_URL is unset, it derives the frontend URL from the first non-localhost entry in IRIS_CORS_ORIGINS (set since v6.0.0, guaranteed present — the frontend can't call iris-api without it). Resolution order: IRIS_WEB_URLIRIS_CORS_ORIGINS (first non-localhost) → API issuer URL.

Tests

40/40 backend OAuth tests pass. 4 new regression cases pin the new fallback path.

Deploy

  • iris-api auto-deploys on push to main.
  • No env-var changes needed in Render — the fix works with the existing IRIS_CORS_ORIGINS.
  • No migrations.

User-visible

  • curl https://iris-api-gtb3.onrender.com/.well-known/oauth-authorization-server should report authorization_endpoint: https://iris-uat.chrisbarlow.nz/oauth/authorize once iris-api redeploys.
  • The OAuth flow in claude.ai: tap Sign in → SvelteKit consent page loads (not 404) → Allow → redirect with auth code → token issued → write tools succeed.

See also

v6.0.11 — point OAuth authorization_endpoint at frontend (issue #119)

13 May 18:27
172f867

Choose a tag to compare

Fixed

The OAuth consent page now actually loads (ADR-171). v6.0.10 unblocked the OAuth trigger — tapping Sign in on the Iris connector finally initiated the flow. But the browser then redirected to the AS-metadata-advertised authorization_endpoint and landed on a hard {"detail":"Not Found"} 404. The metadata advertised the API host, but iris-api has no GET handler at /oauth/authorize — the user-facing consent screen is a SvelteKit page on the frontend at https://iris-uat.chrisbarlow.nz/oauth/authorize.

v6.0.11 sources authorization_endpoint from IRIS_WEB_URL (the frontend host) in backend/app/oauth/router.py. Token / registration / revocation endpoints stay on the API host — they're machine endpoints with no browser involved.

render.yaml adds IRIS_WEB_URL=https://iris-uat.chrisbarlow.nz to the iris-api service so the live deployment knows where the frontend lives.

User-visible after deploy

  • curl https://iris-api-gtb3.onrender.com/.well-known/oauth-authorization-server now reports authorization_endpoint: https://iris-uat.chrisbarlow.nz/oauth/authorize.
  • claude.ai → tap Sign in on Iris connector → browser opens the SvelteKit consent page (not a 404) → sign in to Iris if not already signed in → consent screen → tap Allow → redirected back to claude.ai with auth code → bearer issued → write tools work.

Tests

36/36 backend OAuth tests pass. 3 new regression cases pin the URL sourcing, trailing-slash stripping, and that machine endpoints stay on the API host.

Deploy

  • Render auto-deploys iris-api on push to main. The new IRIS_WEB_URL env var lands with the deploy.
  • No migrations.

See also

v6.0.10 — 401+WWW-Authenticate on unauth MCP requests (issue #119)

13 May 10:46
8ca0590

Choose a tag to compare

Fixed

OAuth-Discovery trigger now fires for claude.ai's MCP client (ADR-170). The MCP authorization spec (2025-06-18) and RFC 9728 require resource servers to return HTTP 401 with WWW-Authenticate: Bearer resource_metadata="..." whenever a request lacks credentials. That 401 is the canonical trigger that causes claude.ai (and any compliant MCP client) to fetch the metadata, DCR-register itself, redirect the user to sign in, exchange the code for a bearer, and retry.

iris-mcp v6.0.0 through v6.0.9 returned HTTP 200 with a JSON tool-error body for unauthenticated requests. The spec's 401 trigger never fired, claude.ai treated the Iris connector as anonymous, and the user-visible "Sign in" button never appeared.

v6.0.10 short-circuits POST / at the transport layer with a spec-compliant 401 + WWW-Authenticate when the request has no bearer token. The resource_metadata URL sources from IRIS_MCP_PUBLIC_URL (when set) or IRIS_API_URL as fallback. Static/health endpoints (/info, /favicon.*, /.well-known/oauth-protected-resource) remain anonymous.

Removed

Anonymous HTTP read access via iris-mcp. CLI scripts that want anonymous reads must use the stdio transport (iris-mcp with IRIS_TOKEN) or talk to iris-api directly. The frontend's read-only public endpoints and the iris-client SDK are unaffected. This trade-off is what unlocks claude.ai's OAuth flow — every working production hosted-MCP server requires auth uniformly.

User action after deploy

  1. Remove + re-add the Iris connector in claude.ai (Settings → Connectors → Iris → remove → add https://iris-mcp.onrender.com).
  2. The connector card now shows a "Sign in" button. Click it.
  3. Browser tab opens against iris-api's /oauth/authorize. Sign in with the same email/password you use on iris-uat. Consent. Redirect back.
  4. Write tools (create_collection, create_set, etc.) now succeed.

Tests

168/168 MCP tests pass. 4 new TestAuthChallenge cases pin the 401 response shape.

See also

v6.0.9 — fix OAuth metadata URLs + intro/conclusion in TOC (issue #119)

13 May 10:18
de785c8

Choose a tag to compare

Fixed

OAuth auto-sign-in now actually works in claude.ai (ADR-169). The Protected Resource metadata at iris-mcp's /.well-known/oauth-protected-resource advertised the frontend host (iris-uat.chrisbarlow.nz) as the Authorization Server, but /.well-known/oauth-authorization-server and /oauth/* endpoints live on the API host (iris-api-gtb3.onrender.com). The frontend is a SvelteKit SPA that returns its index.html for unknown paths — silently breaking the OAuth discovery chain. claude.ai couldn't parse OAuth metadata from HTML and fell back to surfacing the tool-layer auth_required error to the model. v6.0.0 → v6.0.8 all shipped this bug.

v6.0.9 sources authorization_server from IRIS_API_URL (where the AS endpoints actually live). IRIS_WEB_URL is no longer read by the OAuth path; its purpose is link decoration only. IRIS_MCP_PUBLIC_URL=https://iris-mcp.onrender.com is now set in render.yaml so the live resource field correctly identifies iris-mcp.

Per RFC 7591 Dynamic Client Registration, users do NOT enter a client_id or secret. With the metadata chain correct, claude.ai auto-registers and presents a one-click sign-in popup. The auth_required tool error and the canonical mcp_server_instructions body are reworded to reflect this — old wording assumed the connector UI had a manual OAuth toggle, but the actual flow auto-detects OAuth from Protected Resource metadata and offers a "Sign in" button.

Added

  • Introduction and Conclusion in the Outcomes Theory Book TOC. The set has two root-level markdown diagrams (parent_package_id=null) that bracket Part A through Part J; v6.0.7's package_hierarchy call alone missed them. Canonical doview-book-mcp-system-context.md paste-doc now names two structural-overview calls so the orient covers both packages and root-level diagrams.

Admin action required after deploy

  1. Re-paste the v6.0.9 menu from docs/prompts/doview-book-mcp-system-context.md into the Outcomes Theory Book's mcp_system_context field on the set page.
  2. Re-paste the v6.0.9 auth-recovery body from docs/prompts/mcp-server-instructions.md into the mcp_server_instructions row at /admin/settings/ai.
  3. Remove and re-add the Iris connector in claude.ai so it re-discovers the now-correct OAuth metadata. Then clicking "Sign in" on the connector opens a browser tab for sign-in to Iris.

The v6.0.5 TTL refresh propagates both paste edits within 60s.

Tests

164/164 MCP tests pass. Two new regression cases pin the v6.0.9 metadata correctness.

See also

v6.0.8 — remove ask tool, route analysis/Q&A to local AI (issue #119)

13 May 09:56
a38e430

Choose a tag to compare

Changed

When iris-mcp is consumed by a capable-LLM client (claude.ai / Claude Desktop / Claude Code / Cursor), routing the model's question to Iris' server-side AI is redundant. The ask MCP tool routed cross-scope questions to /api/ai/ask; v6.0.7 testing surfaced the failure mode — the user picked "Generate a DoView analysis" and the model called ask, producing the analysis in a different voice from a different conversation with no follow-through.

  • ask tool removed from the MCP surface. Cross-scope questions are now answered by the local model reading data through search, get_*, list_*, package_hierarchy, and walking the structure in its own voice.
  • apply_diagram_creation description rewritten. Reflects the local-AI-as-author model — drafts come from the client, this tool persists them.
  • Orient wrapper strengthened: two new paragraphs explicitly steer the model to do analysis + Q&A itself, not look for a separate AI tool.
  • Canonical doview-book-mcp-system-context.md paste-doc updated: option 2 broadens from "cross-package via Iris AI" to "cross-package, cross-set, or cross-collection"; option 3 drops the → call create_diagram implementation tag.
  • iris-client.IrisClient.ask(...) SDK method is kept — non-MCP consumers (scripts, jobs, iris-cli) can still use Iris AI directly.

Admin action required

After the deploy, re-paste the v6.0.8 menu from docs/prompts/doview-book-mcp-system-context.md into the Outcomes Theory Book's mcp_system_context field on /admin/settings/ai. The v6.0.5 TTL refresh propagates the change to claude.ai within 60s — no Render restart needed.

Tests

163/163 MCP tests pass. TestAsk replaced with TestAskRemoved; TestWrapperStepsAnalysisToLocalAI added.

Deploy

  • Render service iris-mcp auto-deploys on push to main.
  • /info will report 6.0.8 once deployed.
  • No migrations.

See also

v6.0.7 — TOC as bullet-list with links, verbatim menu (issue #119)

13 May 09:30
fc5a2c9

Choose a tag to compare

Fixed

Two polish issues from v6.0.6 testing:

  1. TOC renders as a markdown bullet list with clickable links per Part / chapter. v6.0.6 got claude.ai to invoke package_hierarchy and present the result, but the model rendered it as a paragraph without links. Causes:

    • package_hierarchy output wasn't decorated with web_url: with_web_urls_list only walks the top level; nested children arrays were left bare.
    • Orient wrapper said only "Surface the resulting tree" — no formatting prescription.

    Fix: new decorate_tree walks a homogeneous tree recursively; new with_web_urls_tree wraps the JSON; _package_hierarchy uses it. Every Part and chapter carries web_url. Orient wrapper now prescribes the format explicitly ("markdown bullet list, ONE ENTRY PER LINE, with each entry as a clickable markdown link using the node's web_url field") with a concrete two-line example.

  2. Menu options stay verbatim. v6.0.6 said "do not paraphrase" but the model still dropped parenthetical examples ("(e.g. J06 — Mathematization of Outcomes Theory)"), tool references ("uses mcp__iris__ask"), and "→ call create_diagram" — and reworded "outcomes-theory analysis" to "outcomes map".

    Fix: orient wrapper now demands "CHARACTER-BY-CHARACTER" copying with explicit negations: "Do NOT summarise. Do NOT shorten. Do NOT drop parenthetical examples ... Long options are intentional."

Tests

161/161 MCP tests pass. 5 new regression cases pin the recursive decoration and the strengthened wrapper wording.

Deploy

  • Render service iris-mcp auto-deploys on push to main.
  • /info will report 6.0.7 once deployed.
  • No migrations.

See also

  • ADR-167 — covers v6.0.6 + v6.0.7 wrapper iterations.
  • Issue #119 — five-revision fix history: v6.0.4 (wire), v6.0.5 (refresh), v6.0.6 (embed), v6.0.7 (TOC format + verbatim menu).

v6.0.6 — orient directive in MCP tool response (issue #119)

13 May 08:26
909a87c

Choose a tag to compare

Fixed

Orient-first protocol now reaches claude.ai's hosted MCP integration (issue #119, ADR-167).

v6.0.4 wired Server.instructions through HTTP; v6.0.5 kept it fresh via TTL refresh. Both verified delivering the strong canonical body to claude.ai on the wire. But claude.ai's model still skipped package_hierarchy and paraphrased the menu.

Root cause: claude.ai's hosted MCP integration does not reliably surface InitializeResult.instructions to the model. The ADR-163 architecture (centralize orient in Server.instructions) is broken for claude.ai specifically.

Fix

iris-mcp now re-embeds the orient directive directly into the tool RESPONSE body, prepended to any non-empty mcp_system_context field on set / collection items. The directive pre-fills the scope's id in the tool-call signature (set_id="..." / collection_id="...") so the model has the exact package_hierarchy(set_id="...") call ready — no inference needed, just execute.

The directive is hardcoded in iris-mcp (universal protocol, not scope-specific). Admin-edits to mcp_system_context keep focusing on the per-scope menu. The Server.instructions channel is preserved as belt-and-suspenders for clients that do surface it (Claude Desktop, Claude Code, Cursor).

Wiring

  • New wrap_orient(item, kind) primitive in mcp/src/iris_mcp/links.py.
  • with_web_url, with_web_urls_list, with_web_urls_search all call it for every set/collection in their payload.
  • Idempotent via marker prefix check.
  • Always-on regardless of IRIS_WEB_URL.

Tests

  • test_links_orient_wrapper.py — 18 new cases.
  • test_links_passes_mcp_system_context.py — 4 v5.11.0 / ADR-156 passthrough cases updated.
  • 154/154 MCP tests pass.

Deploy

  • Render service iris-mcp auto-deploys on push to main.
  • /info will report 6.0.6 once deployed.
  • No migrations.

See also