Releases: cgbarlow/iris
v6.0.15 — decorate create_* tool responses with web_url (ADR-175)
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)
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
- Set
IRIS_JWT_SECRETon the iris-api service in the Render dashboard (Environment → Add Environment Variable → value:openssl rand -hex 32output). - 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
- ADR-174 — Accept iris-OAuth JWTs in Supabase deployment mode
- Issue #119 — eleven-revision history.
v6.0.13 — fix OAuth token-exchange Postgres bool-vs-int crash (issue #119)
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
- ADR-173 — Parameterise booleans for SQLite/Postgres portability in OAuth service
- Issue #119 — ten-revision fix history (v6.0.4 → v6.0.13).
v6.0.12 — derive OAuth frontend URL from CORS_ORIGINS as fallback (issue #119)
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_URL → IRIS_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-servershould reportauthorization_endpoint: https://iris-uat.chrisbarlow.nz/oauth/authorizeonce 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
- ADR-172 — Derive OAuth authorization_endpoint frontend URL from CORS origins
- ADR-171 — the v6.0.11 fix this refines.
- Issue #119 — nine-revision fix history.
v6.0.11 — point OAuth authorization_endpoint at frontend (issue #119)
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-servernow reportsauthorization_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 newIRIS_WEB_URLenv var lands with the deploy. - No migrations.
See also
- ADR-171 — AS authorization_endpoint must point at the frontend, not the API
- Issue #119 — eight-revision fix history (v6.0.4 → v6.0.11).
v6.0.10 — 401+WWW-Authenticate on unauth MCP requests (issue #119)
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
- Remove + re-add the Iris connector in claude.ai (Settings → Connectors → Iris → remove → add
https://iris-mcp.onrender.com). - The connector card now shows a "Sign in" button. Click it.
- Browser tab opens against iris-api's
/oauth/authorize. Sign in with the same email/password you use on iris-uat. Consent. Redirect back. - 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
- ADR-170 — Require OAuth bearer on the MCP HTTP endpoint
- Issue #119 — seven-revision fix history (v6.0.4 → v6.0.10).
v6.0.9 — fix OAuth metadata URLs + intro/conclusion in TOC (issue #119)
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'spackage_hierarchycall alone missed them. Canonicaldoview-book-mcp-system-context.mdpaste-doc now names two structural-overview calls so the orient covers both packages and root-level diagrams.
Admin action required after deploy
- Re-paste the v6.0.9 menu from
docs/prompts/doview-book-mcp-system-context.mdinto the Outcomes Theory Book'smcp_system_contextfield on the set page. - Re-paste the v6.0.9 auth-recovery body from
docs/prompts/mcp-server-instructions.mdinto themcp_server_instructionsrow at/admin/settings/ai. - 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
- ADR-169 — Fix Authorization Server URL in Protected Resource metadata
- Issue #119
v6.0.8 — remove ask tool, route analysis/Q&A to local AI (issue #119)
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.
asktool removed from the MCP surface. Cross-scope questions are now answered by the local model reading data throughsearch,get_*,list_*,package_hierarchy, and walking the structure in its own voice.apply_diagram_creationdescription 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.mdpaste-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_diagramimplementation 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-mcpauto-deploys on push tomain. /infowill report6.0.8once deployed.- No migrations.
See also
- ADR-168 — Remove ask tool from MCP surface
- Issue #119 — six-revision fix history.
v6.0.7 — TOC as bullet-list with links, verbatim menu (issue #119)
Fixed
Two polish issues from v6.0.6 testing:
-
TOC renders as a markdown bullet list with clickable links per Part / chapter. v6.0.6 got claude.ai to invoke
package_hierarchyand present the result, but the model rendered it as a paragraph without links. Causes:package_hierarchyoutput wasn't decorated withweb_url:with_web_urls_listonly walks the top level; nestedchildrenarrays were left bare.- Orient wrapper said only "Surface the resulting tree" — no formatting prescription.
Fix: new
decorate_treewalks a homogeneous tree recursively; newwith_web_urls_treewraps the JSON;_package_hierarchyuses it. Every Part and chapter carriesweb_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'sweb_urlfield") with a concrete two-line example. -
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-mcpauto-deploys on push tomain. /infowill report6.0.7once 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)
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 inmcp/src/iris_mcp/links.py. with_web_url,with_web_urls_list,with_web_urls_searchall 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-mcpauto-deploys on push tomain. /infowill report6.0.6once deployed.- No migrations.
See also
- ADR-167 — Embed orient directive in MCP tool response
- SPEC-167-A
- Issue #119 — four-revision fix history: v6.0.4 (wire), v6.0.5 (refresh), v6.0.6 (embed).