All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.4.0 - 2026-06-11
This release redesigns OAuth authorization around a pluggable authorizer that owns the full
token-verification decision, and adds per-request resolution of the authorization server and
protected resource metadata. The authorization changes are breaking relative to 0.3.0; see
Changed for how to migrate.
Added
- Per-request OAuth authorization server resolution:
authorization_serversmay now be afn conn -> [issuer] endresolver, allowing tenant/realm-aware Protected Resource Metadata. Token audience/resource binding is owned by the configured authorizer. - Per-request OAuth protected resource metadata URL resolution via
resource_metadata_url: fn conn -> url end, allowingWWW-Authenticatechallenges to preserve tenant context (e.g. a?realm=...query parameter) for the follow-up metadata request. The discovery document is served only at the static well-known paths derived from:resource; a resolver that points at a different path (e.g. a per-tenant path segment) must be served by your own route or an external host.
Changed
The following are breaking relative to 0.3.0.
Urchin.Authnow delegates the full request authorization decision to an injectedUrchin.Auth.Authorizer(authorize/3) or 3-arity function. Urchin extracts bearer tokens, serves metadata, buildsWWW-Authenticatechallenges and passes claims to handlers; token validity, expiry, issuer, audience/resource binding, scopes and tenant policy are owned by the authorizer. A missing or blank token is resolved by Urchin to a401:missingchallenge before the authorizer runs, so the authorizer is only invoked with a non-empty token and the unauthenticated discovery bootstrap always gets the spec challenge.- BREAKING / SECURITY: Urchin no longer performs SDK-level expiry, audience/resource
binding or request-scope enforcement after a token callback succeeds. Migrating
applications must implement those checks inside their authorizer (for example using
Urchin.Auth.Claims.covers_resource?/2andhas_scopes?/2). Urchin.Auth.new!/1now rejects removed options such as:token_validatorand:audience_validation, and rejects unknown options instead of silently ignoring them.Urchin.Auth.Authorizermay return{:ok, map}for migration compatibility; Urchin normalizes the map (string- or atom-keyed) throughUrchin.Auth.Claims.from_map/1. A foreign struct returned in{:ok, ...}is reported as a server error rather than crashing the authorization pipeline.- Authorization server issuer URLs now reject query strings in addition to fragments.
- Extra
:metadatafields may no longer override fields owned by Urchin, such asresourceandauthorization_servers. - Protected Resource Metadata responses now include
Cache-Control: no-store.
Fixed
- A per-request
:resource_metadata_urlor:required_scopesresolver that raises while aWWW-Authenticatechallenge is being built no longer escalates the401/403into a500with no challenge; the challenge degrades to a valid header without the failed hint, and the failure is logged. The scope hint is also resolved only when a challenge is built, not on every successful request.
0.3.0 - 2026-06-08
This release makes the server enforce the MCP specification by default. Several behaviors that were previously absent or lenient are now always on; see Changed for the breaking details and how to adapt.
Added
- Session lifecycle limits:
:max_sessions(reject new sessions with503past a cap — enforced atomically, before the server'sinit/1runs, so a rejected session pays no init cost and the cap holds under concurrent initializes),:session_idle_timeout(terminate after inactivity; a session serving a request is not reaped) and:session_max_lifetime(terminate a fixed time after creation). All default tonil(unlimited) and are validated as positive integers at startup. Session processes are nowrestart: :temporaryso an ended session is never resurrected under its old id. - Declarative tool scopes:
tool "name", scopes: ["files:write"], ...enforces the scopes againstctx.authbefore the handler runs, failing closed when the request carries no authorization. :expose_internal_errorstransport option (defaultfalse). Unexpected exceptions and malformed handler returns are now logged in full but return a generic message to the client; enable the option to surface the detail in development. DeliberateUrchin.Errorvalues and{:error, message}returns are never redacted — theirmessage/datareach the client unchanged.- Capability guards:
Urchin.Context.create_message/3,elicit/3andlist_roots/2return an error without contacting the client when it did not advertise the matchingsampling/elicitation/rootscapability. 415 Unsupported Media Typefor POST requests whoseContent-Typeis notapplication/json.SECURITY.mdwith a threat model, deployment checklist and vulnerability reporting.:sse_buffer_limittransport option (defaultnil, preserving the session's internal default of100) forwarding the per-session GET-stream replay buffer size to the session; previously only configurable onUrchin.Sessiondirectly.
Changed
The following are now enforced by default, with no opt-out, for MCP spec compliance. They are
breaking relative to 0.2.0.
- A DSL tool's
tools/callarguments are validated against itsinput_schemabefore the handler runs; a mismatch is returned as aCallToolResultwithisError: trueso the model can self-correct. A tool that declares noinput_schemanow defaults to an object that accepts no properties (additionalProperties: false), so unexpected arguments are rejected — declare an explicitinput_schemato accept arbitrary fields. A non-objectargumentsvalue is a malformed request and remains a JSON-RPCinvalid_paramserror. Servers that implementcall_tool/3by hand validate their own arguments.Urchin.Schemaimplements the supported (minimal) JSON Schema subset. - Operation requests received before the client sends
notifications/initializedare rejected withinvalid_request; onlypingis allowed before initialization (the lifecycle's pings-and-logging exception is for the server's own requests, not the client'slogging/setLevel). Clients must complete the lifecycle handshake before issuing other requests. - A
tools/callhandler's{:error, message}(string) is returned as aCallToolResultwithisError: trueso the model can self-correct. A protocol error returned as{:error, %Urchin.Error{}}is always a JSON-RPC error. (Previously a string handler error became a JSON-RPC internal error.) - Duplicate literal tool names within a server are rejected at compile time (a silently shadowed duplicate was previously accepted, with the last declaration winning); non-literal names (a variable or expression) cannot be compared statically and are not checked. Urchin enforces no tool-name pattern (the MCP schema imposes none); servers should still follow the MCP naming recommendations.
initializerequiresprotocolVersion(string),capabilities(object) andclientInfo(with a stringnameandversion); a missing or mistyped field is aninvalid_paramserror rather than a silently-defaulted value. The server'sserverInfomust likewise carry a stringnameandversion.- The
MCP-Protocol-Versionheader is validated onDELETE, matchingPOSTandGET. completion/completerequest params are validated (refas aref/prompt/ref/resourceunion,argument.name/valueas strings,context.argumentsvalues as strings) and returninvalid_paramswhen malformed. Results are capped at 100 values — a handler returning more is truncated to the top 100 (already ranked by relevance) withhasMoreset — and a non-conforming result shape (non-stringvalues, etc.) is an internal error.- A tool's
input_schemaandoutput_schemamust be JSON Schema objects whose roottypeis"object"(per the MCP tools spec); the DSL rejects a non-conforming schema at compile time. logging/setLevelis now a library builtin: when the server advertises theloggingcapability (viause Urchin.Server, logging: true) it succeeds and applies the level to the session even without aset_log_level/2callback. The level is validated against the MCP log levels (invalid_paramsotherwise), an exportedset_log_level/2still runs as a hook, and the session level is updated only after the hook succeeds. Servers that do not advertiseloggingreturnmethod_not_found.
Fixed
- README no longer claims unqualified "resumable SSE streams"; resumption is scoped to the GET stream, matching the implementation.
0.2.0 - 2026-06-05
Added
- Optional OAuth 2.1 authorization (
Urchin.Auth). Urchin can act as an OAuth 2.1 resource server: RFC 9728 Protected Resource Metadata discovery (served at the well-known URI and advertised viaWWW-Authenticate: resource_metadata), pluggable token validation (Urchin.Auth.TokenValidator), RFC 8707 audience binding, scope enforcement and401/403/400challenges. Enable it with the:authoption on the transport,Urchin.EndpointorUrchin.start_link/2, or compose theUrchin.Auth.PlugandUrchin.Auth.Metadataplugs. Validated claims reach handlers asctx.auth. Authorization remains off by default.
0.1.0 - 2026-06-04
Initial release: a Model Context Protocol (MCP) server library implementing the
2025-11-25 specification over the Streamable HTTP transport.
Added
- Server authoring via the
Urchin.Serverbehaviour and atool/resource/resource_template/promptDSL with automatic capability derivation. - Tools, resources (plus templates and subscriptions), prompts, completion and logging.
- Server-initiated requests over SSE: sampling, elicitation and roots.
- Progress notifications, cancellation, pagination and resumable SSE streams.
- A mountable
Plug(Urchin.Transport.StreamableHTTP) and a standalone Bandit endpoint (Urchin.Endpoint,Urchin.start_link/2), plusUrchin.broadcast/2for fan-out notifications.