Skip to content

feat(streamable_http): add spec resumability (Last-Event-ID replay) - #216

Closed
BobbieBarker wants to merge 124 commits into
zoedsoupe:mainfrom
fypm-labs:pr/streamable-http-resumability
Closed

BobbieBarker wants to merge 124 commits into
zoedsoupe:mainfrom
fypm-labs:pr/streamable-http-resumability

Conversation

@BobbieBarker

Copy link
Copy Markdown
Contributor

Stacked on #215. This branch includes the #215 flap-fix commit as its base, so the diff shows two commits; please review the second one (feat(streamable_http): add spec resumability). Once #215 merges I'll rebase this to a clean single-commit diff. The resumability stream lifecycle relies on #215's takeover-without-close semantics.

Problem

The Streamable HTTP transport does not implement the MCP spec's "Resumability and Redelivery" feature. Clients that drop and reconnect the standalone GET SSE stream lose any server-to-client messages sent during the gap, and the transport never honors the Last-Event-ID header the spec defines for replay.

Solution

Add opt-in resumability for the standalone SSE stream:

  • New EventStore behaviour (append / replay / latest_id / delete / child_spec, optional resolve_name/2), wired via the transport's :event_store option using the same {module, opts} shape as :task_store.
  • Default bounded in-memory adapter (EventStore.InMemory): session-scoped monotonic ids, per-session ring (:history_size), LRU session cap (:max_sessions), validated bounds.
  • Priming event on every stream open (client holds a cursor from the first byte) with a configurable SSE retry: field.
  • Last-Event-ID parsing on GET and in-order replay of recorded events before live delivery. The dedupe floor is seeded from the ids actually replayed (not the client cursor), so a stale/reset cursor never drops live events.
  • Recording survives reconnect gaps (records even with no handler attached), bounded by a :stream_grace close timer so dropped sessions cannot leak or thrash the store.
  • Fail-closed on store errors: a failed append is not delivered with a bogus legacy id, and a failed replay aborts the stream rather than faking a resume.
  • Fully opt-in: absent :event_store keeps the exact legacy per-connection id behavior; the deprecated 2024-11-05 SSE transport is unaffected.

Note: the per-request POST response stream now sends no id: line (was id: 0), since per-request streams must not carry a session-scoped resumption cursor per the spec.

Rationale

Resumability and Redelivery via Last-Event-ID is an explicit, named part of the MCP Streamable HTTP transport spec (2025-03-26 / 2025-11-25). The design mirrors the existing :task_store pattern (pluggable behaviour + batteries-included in-memory default + opt-in wiring) so it stays idiomatic and preserves legacy behavior byte-for-byte when unconfigured. Relates to the session-recovery pain in #204 / #175.

Coordination

The supervisor child-assembly overlaps #188 (both add to build_http_children). The two are additive and compose cleanly; if #188 lands first I'll rebase the event-store child on top.

Tests

  • event_store/in_memory_test.exs: id monotonicity/isolation, replay ordering and bounded-history gaps, delete idempotency, LRU session eviction, bound validation.
  • resumability_test.exs: priming (fresh / high-water / retry), replay-then-live ordering, exactly-once dedupe across the register/replay race, stale-cursor safety, transport recording incl. reconnect-gap and append-failure fail-closed, grace-timer close/keep, and end-to-end Plug Last-Event-ID wiring.

emilwojtaszek and others added 30 commits November 24, 2025 10:57
Co-authored-by: Claude <noreply@anthropic.com>
Bumps [plug](https://github.com/elixir-plug/plug) from 1.18.1 to 1.19.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/elixir-plug/plug/blob/main/CHANGELOG.md">plug's
changelog</a>.</em></p>
<blockquote>
<h2>v1.19.0 (2025-12-08)</h2>
<p>This release requires Elixir v1.14+ and it bumps the recommended
:strong and :compatible SSL/TLS ciphers suite to align with modern
security standards, prioritizing TLS 1.3 and 1.2. Support for the
insecure TLS 1.0 and 1.1 protocols are removed in accordance with RFC
8996.</p>
<h3>Enhancements</h3>
<ul>
<li>[Plug.Router] Allow colon for named segments to be escaped</li>
<li>[Plug.SSL] Prioritize TLS 1.3 and 1.2 ciphers</li>
<li>[Plug.SSL] Allow excluding redirects based on hosts, paths, or the
connection</li>
<li>[Plug.Static] Add <code>:raise_on_missing_only</code></li>
<li>[Plug.Upload] Partition the uploader to improve performance</li>
<li>[Plug.Upload] Add API for deleting files</li>
</ul>
<h3>Deprecations</h3>
<ul>
<li>[Plug.Conn.Adapter] Deprecate <code>:owner</code> field</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/elixir-plug/plug/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=plug&package-manager=hex&previous-version=1.18.1&new-version=1.19.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [styler](https://github.com/adobe/elixir-styler) from 1.9.1 to
1.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/adobe/elixir-styler/releases">styler's
releases</a>.</em></p>
<blockquote>
<h2>v1.10.0</h2>
<h3>Improvements</h3>
<p>Two new standard-library pipe optimizations</p>
<ul>
<li><code>enum |&gt; Enum.map(fun) |&gt;
Enum.intersperse(separator)</code> =&gt;
<code>Enum.map_intersperse(enum, separator, fun)</code></li>
<li><code>enum |&gt; Enum.sort() |&gt; Enum.reverse()</code> =&gt;
<code>Enum.sort(enum, :desc)</code></li>
</ul>
<p>And Req (the http client library) pipe optimizations, as detailed
below</p>
<h4>Req pipe optimizations</h4>
<p><a href="https://github.com/wojtekmach/req">Req</a> is a popular HTTP
Client. If you aren't using it, you can just ignore this whole
section!</p>
<p>Reqs 1-arity &quot;execute the request&quot; functions (<code>delete
get head patch post put request run</code>) have a 2-arity version that
takes a superset of the arguments <code>Req.new/1</code> does as its
first argument, and the typical <code>options</code> keyword list as its
second argument. And so, many places developers are calling a 1-arity
function can be replaced with a 2-arity function.</p>
<p>More succinctly, these two statements are equivalent:</p>
<ul>
<li><code>foo |&gt; Req.new() |&gt; Req.merge(bar) |&gt;
Req.post!()</code></li>
<li><code>Req.post!(foo, bar)</code></li>
</ul>
<p>Styler now rewrites the former to the latter, since &quot;less is
more&quot; or &quot;code is a liability&quot;.</p>
<p>It also rewrites <code>|&gt; Keyword.merge(bar) |&gt;
Req.foo()</code> to <code>|&gt; Req.foo(bar)</code>. <strong>This
changes the program's behaviour</strong>, since
<code>Keyword.merge</code> would overwrite existing values in all cases,
whereas <code>Req</code> 2-arity functions intelligently deep-merge
values for some keys, like <code>:headers</code>.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/adobe/elixir-styler/blob/main/CHANGELOG.md">styler's
changelog</a>.</em></p>
<blockquote>
<h2>1.10.0</h2>
<h3>Improvements</h3>
<p>Two new standard-library pipe optimizations</p>
<ul>
<li><code>enum |&gt; Enum.map(fun) |&gt;
Enum.intersperse(separator)</code> =&gt;
<code>Enum.map_intersperse(enum, separator, fun)</code></li>
<li><code>enum |&gt; Enum.sort() |&gt; Enum.reverse()</code> =&gt;
<code>Enum.sort(enum, :desc)</code></li>
</ul>
<p>And Req (the http client library) pipe optimizations, as detailed
below</p>
<h4>Req pipe optimizations</h4>
<p><a href="https://github.com/wojtekmach/req">Req</a> is a popular HTTP
Client. If you aren't using it, you can just ignore this whole
section!</p>
<p>Reqs 1-arity &quot;execute the request&quot; functions (<code>delete
get head patch post put request run</code>) have a 2-arity version that
takes a superset of the arguments <code>Req.new/1</code> does as its
first argument, and the typical <code>options</code> keyword list as its
second argument. And so, many places developers are calling a 1-arity
function can be replaced with a 2-arity function.</p>
<p>More succinctly, these two statements are equivalent:</p>
<ul>
<li><code>foo |&gt; Req.new() |&gt; Req.merge(bar) |&gt;
Req.post!()</code></li>
<li><code>Req.post!(foo, bar)</code></li>
</ul>
<p>Styler now rewrites the former to the latter, since &quot;less is
more&quot; or &quot;code is a liability&quot;.</p>
<p>It also rewrites <code>|&gt; Keyword.merge(bar) |&gt;
Req.foo()</code> to <code>|&gt; Req.foo(bar)</code>. <strong>This
changes the program's behaviour</strong>, since
<code>Keyword.merge</code> would overwrite existing values in all cases,
whereas <code>Req</code> 2-arity functions intelligently deep-merge
values for some keys, like <code>:headers</code>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/adobe/elixir-styler/commit/70b50451a8ded14ed8364ba26a5958f05220446e"><code>70b5045</code></a>
v1.10.0</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/2af7d19948f2bfd78780ec38896086daebf048e1"><code>2af7d19</code></a>
Enum.map |&gt; Enum.intersperse =&gt; Enum.map_intersperse</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/7884561a1294c3fbe36689c40851bab444cda078"><code>7884561</code></a>
allow docs for Styler.string_to_ast</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/a490ad68bce097da65b0258f850a702035238c87"><code>a490ad6</code></a>
sort |&gt; reverse =&gt; sort(:desc)</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/78ced6b4dc2c72df34dbb44973555bc8dfcf3e36"><code>78ced6b</code></a>
TIL capital sigils cant be escaped</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/1a6a375a6fc2148f778d5f9da04b8f5b6bbcc5b1"><code>1a6a375</code></a>
tweak intro sentence</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/e48ca6cada3d7cd90b624609827ef0af26bf96fe"><code>e48ca6c</code></a>
less is more</li>
<li><a
href="https://github.com/adobe/elixir-styler/commit/793cf27938a99c1492b0ce611519d91fda68311a"><code>793cf27</code></a>
optimize Req pipes</li>
<li>See full diff in <a
href="https://github.com/adobe/elixir-styler/compare/v1.9.1...v1.10.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=styler&package-manager=hex&previous-version=1.9.1&new-version=1.10.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.39.1 to
0.39.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md">ex_doc's
changelog</a>.</em></p>
<blockquote>
<h2>v0.39.2 (2025-12-04)</h2>
<ul>
<li>Bug fixes
<ul>
<li>Do not strip hrefs on summaries</li>
<li>Show go to latest for prereleases</li>
<li>Prevent fake italic in autocomplete text</li>
<li>Rename &quot;Search Hexdocs&quot; link to &quot;Go to package
docs&quot;</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/425378e393769a857dd414255cc83be82e5f079e"><code>425378e</code></a>
Release v0.39.2</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/10d8315f7ba2e0e55093580707ff5788fa889922"><code>10d8315</code></a>
Ensure IDs rather than hrefs are stripped, closes <a
href="https://github.com/elixir-lang/ex_doc/issues/2175">#2175</a></li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/72bb755a90c9a98b645e7e923ebd3bb85ead8f32"><code>72bb755</code></a>
Show go to latest for prereleases, closes <a
href="https://github.com/elixir-lang/ex_doc/issues/2173">#2173</a></li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/6db9cab2742a19488376e870060f62a589bec672"><code>6db9cab</code></a>
Fix docs: Move <code>source_url</code> to <code>project</code> (<a
href="https://github.com/elixir-lang/ex_doc/issues/2172">#2172</a>)</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/e7abbaedb88eed88858f85b53862801e5f456a32"><code>e7abbae</code></a>
Add Elixir v1.19 and Erlang/OTP 28 to CI (<a
href="https://github.com/elixir-lang/ex_doc/issues/2166">#2166</a>)</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/60203be6699d00abef9e054bdcd0669fbca7ae43"><code>60203be</code></a>
Update assets</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/a4927a41cd3d6becc3a4e9540fb35ed6c5fb9403"><code>a4927a4</code></a>
Prevent fake italic in autocomplete text (<a
href="https://github.com/elixir-lang/ex_doc/issues/2168">#2168</a>)</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/c8d1e682a4dc7a1355103b858534f46734f5c78c"><code>c8d1e68</code></a>
Rename Search Hexdocs link to Go to package docs</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/33b1ffca53807808ae23fc915f54cd13bd5c5920"><code>33b1ffc</code></a>
Release v0.39.1</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/5bb6c18048c4f7c314fc13e4a424fa6c45e9eebb"><code>5bb6c18</code></a>
Update assets</li>
<li>Additional commits viewable in <a
href="https://github.com/elixir-lang/ex_doc/compare/v0.39.1...v0.39.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ex_doc&package-manager=hex&previous-version=0.39.1&new-version=0.39.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Problem

<!-- what problem is the PR is trying to solve? -->

## Solution

<!-- how is the PR solving the problem? -->

## Rationale

<!-- why was it implemented the way it was? -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Improved conditional dependency handling for session storage to ensure
graceful operation when optional libraries are unavailable. No
behavioral changes for existing deployments.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Bumps [credo](https://github.com/rrrene/credo) from 1.7.13 to 1.7.14.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rrrene/credo/releases">credo's
releases</a>.</em></p>
<blockquote>
<h2>v1.7.14</h2>
<p>Check it out on Hex: <a
href="https://hex.pm/packages/credo/1.7.14">https://hex.pm/packages/credo/1.7.14</a></p>
<ul>
<li>Fixed regression for <code>DuplicatedCode</code></li>
<li>Expanded <code>Credo.Check.Warning.ExpensiveEmptyEnumCheck</code> to
cover less obvious cases</li>
<li>New Check: <code>Credo.Check.Warning.StructFieldAmount</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rrrene/credo/blob/master/CHANGELOG.md">credo's
changelog</a>.</em></p>
<blockquote>
<h2>1.7.14</h2>
<ul>
<li>Fixed regression for <code>DuplicatedCode</code></li>
<li>Expanded <code>Credo.Check.Warning.ExpensiveEmptyEnumCheck</code> to
cover less obvious cases</li>
<li>New Check: <code>Credo.Check.Warning.StructFieldAmount</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rrrene/credo/commit/b3a8c4ac73b155c21a02683d37dcb523b74f465d"><code>b3a8c4a</code></a>
Bump version to 1.7.14</li>
<li><a
href="https://github.com/rrrene/credo/commit/677f6389a6d38a957811d1df3b71374ea9081cda"><code>677f638</code></a>
Update CHANGELOG</li>
<li><a
href="https://github.com/rrrene/credo/commit/57deb6fdfbb1e1d3b481a7e395ba732c0e757800"><code>57deb6f</code></a>
Fix warnings for slow usage of <code>length/1</code></li>
<li><a
href="https://github.com/rrrene/credo/commit/ace6edda853c9b9163ab0dc55fca843d8e62633d"><code>ace6edd</code></a>
Fix missing dep</li>
<li><a
href="https://github.com/rrrene/credo/commit/1ffd3b235d1f920a8b54b3baf416af7c9b8b89f7"><code>1ffd3b2</code></a>
Inline pipe</li>
<li><a
href="https://github.com/rrrene/credo/commit/d182b847f282c69ac73355526e87a4dfb0980418"><code>d182b84</code></a>
Add inch_ex</li>
<li><a
href="https://github.com/rrrene/credo/commit/a006b49aa56b9bfce0e1e74ef7ea3c445827c681"><code>a006b49</code></a>
Fix housekeeping workflow</li>
<li><a
href="https://github.com/rrrene/credo/commit/cef51ea8b5c8e0586b51e5da02c51e94d49f2362"><code>cef51ea</code></a>
Refactor ExpensiveEmptyEnumCheck</li>
<li><a
href="https://github.com/rrrene/credo/commit/e781dbb93149c2de4671ed9b76b4bca8d15cb85b"><code>e781dbb</code></a>
Merge branch 'comparison-against-1' of github.com:hauleth/credo into
1226-emp...</li>
<li><a
href="https://github.com/rrrene/credo/commit/c20779e6c09c32d250354bf1262aef8759245f16"><code>c20779e</code></a>
Update Elixir to 1.19.3</li>
<li>Additional commits viewable in <a
href="https://github.com/rrrene/credo/compare/v1.7.13...v1.7.14">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=credo&package-manager=hex&previous-version=1.7.13&new-version=1.7.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: zoey <zoey.spessanha@zeetech.io>
🚀 Want to release this?
---


##
[0.17.0](zoedsoupe/anubis-mcp@v0.16.0...v0.17.0)
(2025-12-09)


### Features

* **redis:** add redix_opts for SSL/TLS support
([zoedsoupe#59](zoedsoupe#59))
([33658ab](zoedsoupe@33658ab))


### Bug Fixes

* added server component description/0 callback
([zoedsoupe#58](zoedsoupe#58))
([a094473](zoedsoupe@a094473))
* redix should be loaded
([zoedsoupe#71](zoedsoupe#71))
([09b872f](zoedsoupe@09b872f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added Redis SSL/TLS support via redix options.

* **Bug Fixes**
  * Fixed server component description callback issue.
  * Fixed redix loading issue.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.40.0 to
0.40.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md">ex_doc's
changelog</a>.</em></p>
<blockquote>
<h2>v0.40.1 (2026-01-31)</h2>
<ul>
<li>
<p>Enhancements</p>
<ul>
<li>Remove link to source from generated .md files</li>
<li>Improve word-breaking of module names and sizing of main page
titles</li>
<li>Include description in llms.txt</li>
</ul>
</li>
<li>
<p>Bug fixes</p>
<ul>
<li>Fix headers in custom groups</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/20a355b005c31f3ba38c7729d52a02571ea245cd"><code>20a355b</code></a>
Release v0.40.1</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/7a71ddf985ca531cc5ab8e0e3c81812209f31cd9"><code>7a71ddf</code></a>
Update assets</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/f44f6fe6aae77960b100cd08878986208449a960"><code>f44f6fe</code></a>
Turn whitespace minification back on</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/38028674ac14570a700ba8804d6ca4cbba1dc951"><code>3802867</code></a>
Improve distinction between docstring headings (H2-H4)</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/e8a46c6fa7d8af5a24af6caf7a266e7995c51ce2"><code>e8a46c6</code></a>
Change headings' levels to match their context</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/9cd866c84ccdd1afa9eee772d72511c5ef99cbae"><code>9cd866c</code></a>
Fix Summary Types heading size</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/e8e74eec1881dae173e02cf8713ff65e88540d68"><code>e8e74ee</code></a>
More word break tests</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/21ec71f709317bdd2e32b6d4d6055857d3761552"><code>21ec71f</code></a>
Update assets</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/8611a164fe24d7b9f669cdad96dcaf30e8254fc1"><code>8611a16</code></a>
Improve word-breaking of module names and sizing of main page titles (<a
href="https://github.com/elixir-lang/ex_doc/issues/2190">#2190</a>)</li>
<li><a
href="https://github.com/elixir-lang/ex_doc/commit/1b1fe51e479b9bcbf72802e7ef700ed3cedf53ac"><code>1b1fe51</code></a>
Bump lodash from 4.17.21 to 4.17.23 in /assets (<a
href="https://github.com/elixir-lang/ex_doc/issues/2187">#2187</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/elixir-lang/ex_doc/compare/v0.40.0...v0.40.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ex_doc&package-manager=hex&previous-version=0.40.0&new-version=0.40.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary

This fixes a race condition where responses could be silently lost when
an SSE handler process died but the transport hadn't yet processed the
`:DOWN` message.

**The issue:**
1. SSE handler process dies (network drop, crash, etc.)
2. `:DOWN` message is queued to transport GenServer
3. Before transport processes `:DOWN`, a new request calls
`get_sse_handler`
4. `get_sse_handler` returns the stale PID
5. `send/2` silently drops the message to the dead process
6. Client receives HTTP 202 but never gets the actual response

**The fix:**
- Add `Process.alive?` check in `route_sse_response` before sending
- If the handler is stale, clean up the entry and establish a new SSE
connection

## Test plan

- [x] All existing tests pass (519 tests, 0 failures)
- [x] Verified fix in production application (Flux MCP server)

🤖 Generated with [Claude Code](https://claude.ai/code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Improvements**
* Server-Sent Events connections are now more resilient with enhanced
validation and automatic recovery for stale connections, significantly
improving real-time communication reliability and reducing message
delivery failures in active use.

* **Configuration**
* Session logging level is now optional, offering greater configuration
flexibility and enabling simplified setup when explicit specification is
not required.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Problem

The codebase has version-specific protocol logic scattered across
multiple modules (Anubis.Protocol,
Anubis.MCP.Message, Anubis.Server.Base, Anubis.Server). Adding support
for a new MCP spec version means touching many
places, and the server macro module had incorrect hardcoded version
strings (2024-05-11, 2024-10-07) that don't exist
in the supported versions list. This is Phase 1 of the architecture
refactor plan addressing issues hermes#179 and
hermes#141.

## Solution

- Created Anubis.Protocol.Behaviour defining callbacks each version
module must implement (version, features, schemas,
 methods)
- Created per-version modules (V2024_11_05, V2025_03_26, V2025_06_18)
under lib/anubis/protocol/ encoding
version-specific schemas, features, and method lists
- Created Anubis.Protocol.Registry as the central dispatch point mapping
version strings to modules, with negotiation
support
- Refactored Anubis.Protocol to delegate to the Registry while
preserving its entire public API (zero breaking
changes)
- Updated Server.Base to use Registry.negotiate/2 instead of a private
negotiation function
- Fixed Anubis.Server macro to derive @protocol_versions from the
Registry instead of a hardcoded (incorrect) list
- Stored the negotiated protocol module in Session, Client.State, and
Frame.private for downstream use in later phases
- Added 64 new tests covering the Registry, version modules, behaviour
compliance, feature inheritance, and backward
compatibility

## Rationale

Version modules inherit from their predecessor (V2025_03_26 delegates to
V2024_11_05 for unchanged schemas) to avoid
duplication while making differences explicit. The Registry is a
compile-time map (not a GenServer) since version data
is static. Anubis.Protocol's public API was preserved via defdelegate
and thin wrappers so this is a purely internal
refactor — no downstream code changes required. Storing the resolved
protocol module in session/connection state
enables later phases to dispatch version-specific logic without
re-resolving.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added multi-version MCP protocol support with dynamic version
negotiation and fallback handling.
* Introduced version-aware feature discovery to identify capabilities
across supported protocol versions.
* Enabled automatic protocol module resolution during client
initialization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oedsoupe#95)

## Problem

Transport modules are tightly coupled to GenServer processes, making
them impossible to use as pure functions for
message parsing/encoding. This blocks Phase 3/4 of the architecture
refactor where transports need to be called
directly from Session processes and Plug pipelines without a separate
transport process.

## Solution

Implement the functional `Anubis.Transport` behaviour callbacks
(`transport_init/1`, `parse/2`, `encode/2`,
`extract_metadata/2`) across all 6 transport modules:

- `Anubis.Transport.STDIO` — newline-delimited JSON with partial message
buffering
- `Anubis.Transport.StreamableHTTP` — JSON with batch array support
- `Anubis.Transport.SSE` — JSON string/map parsing
- `Anubis.Server.Transport.STDIO` — same as client, with server metadata
- `Anubis.Server.Transport.StreamableHTTP` — JSON with Plug.Conn
metadata extraction
- `Anubis.Server.Transport.SSE` — SSE event format encoding, Plug.Conn
metadata

Added 64 unit tests covering parse/encode round-trips, buffering,
batching, metadata extraction, and cross-transport
consistency.

## Rationale

Functional callbacks are added alongside existing GenServer code (no
breaking changes). The GenServer transport
processes remain for backward compatibility — they will be removed in
Phase 3 (Server) and Phase 4 (Client) when the
architecture shifts to Session-owns-transport. This incremental approach
keeps all existing tests passing while
preparing the foundation for the next phases.
🚀 Want to release this?
---


##
[0.17.1](zoedsoupe/anubis-mcp@v0.17.0...v0.17.1)
(2026-02-28)


### Bug Fixes

* Check Process.alive? before sending to SSE handler
([zoedsoupe#82](zoedsoupe#82))
([e1dc705](zoedsoupe@e1dc705))


### Code Refactoring

* **phase-1:** abstract protocol version negotiation
([zoedsoupe#93](zoedsoupe#93))
([05a2362](zoedsoupe@05a2362))
* **phase-2:** transport layer as functions, backward compatible
([zoedsoupe#95](zoedsoupe#95))
([105d6a9](zoedsoupe@105d6a9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
…soupe#96)

## Problem

The server architecture had a single-GenServer bottleneck
(`Server.Base`) that serialized all requests across all sessions through
one mailbox. The `Frame` struct accumulated internal state (`transport`,
`request`, `private`, `initialized`) that leaked implementation details
to users and created transport-dependent branching in user code.

Related: https://github.com/cloudwalk/hermes-mcp/issues/179,
https://github.com/cloudwalk/hermes-mcp/issues/#141, RFC zoedsoupe#24

## Solution

**Phase 3 — Session-Centric Architecture**

- Each MCP session is now an independent GenServer process under
`Session.Supervisor` (DynamicSupervisor)
- `Server.Base` becomes a coordinator: session lifecycle management,
expiry timers, and delegation — no longer processes MCP requests
directly
- Transport modules (`StreamableHTTP.Plug`, `SSE.Plug`, `STDIO`) route
requests to the correct Session process via `Registry`
- `Registry` is pluggable (`Registry.Adapter` behaviour) with
`Registry.Local` (ETS) as default, supporting future distributed
implementations (Horde, Phoenix.Tracker)
- Heavy tool execution offloaded to `Task.Supervisor` per session,
keeping the session mailbox responsive

**Phase 3.5 — Context Refactor**

- Introduced `Anubis.Server.Context` — a minimal 4-field struct
(`session_id`, `client_info`, `headers`, `remote_ip`) added as a
read-only field on `Frame`
- Removed `Frame.transport`, `Frame.request`, and `Frame.initialized`
fields — these were internal state that polluted the user-facing struct
- Transport-agnostic design: STDIO naturally has `headers: %{},
remote_ip: nil`, no type branching needed
- Added `Frame.to_saved/1` and `Frame.from_saved/1` for `Session.Store`
integration — context is omitted (request-scoped), only user state
persists
- Session recovery flow: `Registry.lookup` → miss → `Store.load` →
`DynamicSupervisor.start_child` → `Registry.register`
- No process dictionary usage — context is a value passed through Frame,
works naturally with Tasks and async code

## Rationale

**Session-per-process over shared GenServer**: the old design forced all
sessions through one mailbox — a slow tool call in session A blocked
unrelated requests in session B. OTP's model is one process per
concurrent unit of work, and an MCP session is exactly that.

**Context as a Frame field over process dictionary**: `Process.put`
doesn't propagate to `Task.async` spawns, creating silent failures. A
struct field on Frame is explicit, testable, and works everywhere Frame
is passed.

**Minimal Context (4 fields) over full request mirror**: the original
plan had 7+ fields including `protocol_version`, `initialized`,
`request.id`, `request.method`, `request.params`, and transport type
unions. Most duplicated data already available through callback
arguments or enforced by Session lifecycle. Users should never branch on
transport type — that defeats transport-agnostic design.

**Registry and Store as orthogonal concerns**: Registry answers "where
is the process?" (ephemeral, dies with process). Store answers "what was
the state?" (durable, survives restarts). Both are optional and
pluggable independently.

---

**Next steps**:
- Phase 4 — Client refactor: decompose `Client.Base` (1634 LOC) into
`Client.Handlers` + `Client.Sampling`
- Phase 5 — Dead code removal, deduplication audit, and documentation
fixes (including `CONTRIBUTING.md` license mismatch, outdated API
examples in hexdocs pages, missing `pages/home.md`)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Error responses now follow JSON‑RPC 2.0 envelope with a top-level
"jsonrpc": "2.0".
* **Refactor**
* Notification API simplified — sending notifications no longer requires
passing per-frame state.
* Sessions moved to a session-centric model with session-based routing
for transports.
* Registry and naming model modernized for deterministic
transport/session names and pluggable registry options.
* Frame model streamlined to public component maps and a read-only
per-request context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem

`Anubis.Client.Base` module is currently massive, this is a first try to
reduce it

## Solution

Extracted sampling capability and other message handlers (tool calls
responses for ex) to separate modules

## Rationale

N/A


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
  * Updated Zig tooling in release workflows from v0.14.1 to v0.15.2.

* **Refactor**
* Reorganized internal client notification and sampling request handling
into dedicated modules for improved maintainability and separation of
concerns; no changes to external behavior or public interfaces.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Bumps [telemetry](https://github.com/beam-telemetry/telemetry) from
1.3.0 to 1.4.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/beam-telemetry/telemetry/blob/main/CHANGELOG.md">telemetry's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/elixir-telemetry/telemetry/tree/v1.4.1">1.4.1</a></h2>
<h3>Fixed</h3>
<ul>
<li>Avoid crashes when <code>telemetry</code> is invoked before started
(such as during Elixir compile-time)</li>
</ul>
<h2><a
href="https://github.com/elixir-telemetry/telemetry/tree/v1.4.0">1.4.0</a></h2>
<h3>Added</h3>
<ul>
<li>Add <code>telemetry:persist/0</code> which uses persistent term for
faster dispatches (writes are extremely discouraged after persist)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix the <code>telemetry:span_function/0</code> type</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/11462db509623be85c7acf3f15d0579d0d3f4a79"><code>11462db</code></a>
Release v1.4.1</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/11210b44b3bb431059a99847ccaec20352b5bac5"><code>11210b4</code></a>
Do not crash on failed persistent term lookup</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/972ff3bb1b0b95f907d93bbb3726b95eee7b725c"><code>972ff3b</code></a>
Release v1.4.0</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/ddc7f13e4f93d103422f98eb481d3bade2cb73f8"><code>ddc7f13</code></a>
Add &quot;since&quot; doc info and spec to persist/0 (<a
href="https://github.com/beam-telemetry/telemetry/issues/146">#146</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/55d657eb6102b8a81a519e9abca5990506e00ee8"><code>55d657e</code></a>
Remove function allocation and remote call on execution (<a
href="https://github.com/beam-telemetry/telemetry/issues/145">#145</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/c0aff75703636e93dcc581f3dc84c9a7f4dc5870"><code>c0aff75</code></a>
Implement <code>telemetry:persist/0</code> (<a
href="https://github.com/beam-telemetry/telemetry/issues/144">#144</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/614bfb91e9e42e305b95860cc64d237c01435d65"><code>614bfb9</code></a>
Use field name instead of magic number (<a
href="https://github.com/beam-telemetry/telemetry/issues/143">#143</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/13a380ed0214a8f5824c99ef9897db50de84c90c"><code>13a380e</code></a>
Test CI against multiple OTP versions (24-28) (<a
href="https://github.com/beam-telemetry/telemetry/issues/140">#140</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/2f29d755aaf98d1c49a166cf5372755bb7033e72"><code>2f29d75</code></a>
Improve docs for failure events (<a
href="https://github.com/beam-telemetry/telemetry/issues/139">#139</a>)</li>
<li><a
href="https://github.com/beam-telemetry/telemetry/commit/7b99309898cc827fda3f1956e23d34d349453f16"><code>7b99309</code></a>
Fix the telemetry:span_function/0 type (<a
href="https://github.com/beam-telemetry/telemetry/issues/137">#137</a>)</li>
<li>See full diff in <a
href="https://github.com/beam-telemetry/telemetry/compare/v1.3.0...v1.4.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=telemetry&package-manager=hex&previous-version=1.3.0&new-version=1.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [credo](https://github.com/rrrene/credo) from 1.7.15 to 1.7.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rrrene/credo/releases">credo's
releases</a>.</em></p>
<blockquote>
<h2>v1.7.17</h2>
<p>Check it out on Hex: <a
href="https://hex.pm/packages/credo/1.7.17">https://hex.pm/packages/credo/1.7.17</a></p>
<ul>
<li><code>Credo.Check.Readability.ModuleDoc</code> add new param
<code>:ignore_modules_using</code> (defaults to <code>[Credo.Check,
Ecto.Schema, Phoenix.LiveView, ~r/\.Web$/]</code>)</li>
<li><code>Credo.Check.Warning.UnusedOperation</code> update
<code>:modules</code> param: instead of a list of functions to check,
<code>:all</code> can be given to check all functions in a module</li>
<li>New Check:
<code>Credo.Check.Refactor.CondInsteadOfIfElse</code></li>
<li>New Check: <code>Credo.Check.Warning.WrongTestFilename</code></li>
</ul>
<h2>v1.7.16</h2>
<p>Check it out on Hex: <a
href="https://hex.pm/packages/credo/1.7.16">https://hex.pm/packages/credo/1.7.16</a></p>
<ul>
<li>Fix compatibility &amp; compiler warnings with Elixir
1.20.0-rc.1</li>
<li><code>Credo.Check.Refactor.PassAsyncInTestCases</code> add new param
<code>:force_comment_on_explicit_false</code> (defaults to
<code>false</code>)</li>
<li><code>Credo.Check.Warning.Dbg</code> add new param
<code>:allow_captures</code> (defaults to <code>false</code>)</li>
<li>New Check: <code>Credo.Check.Warning.UnusedMapOperation</code></li>
<li>New Check: <code>Credo.Check.Warning.UnusedOperation</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rrrene/credo/blob/master/CHANGELOG.md">credo's
changelog</a>.</em></p>
<blockquote>
<h2>1.7.17</h2>
<ul>
<li><code>Credo.Check.Readability.ModuleDoc</code> add new param
<code>:ignore_modules_using</code> (defaults to <code>[Credo.Check,
Ecto.Schema, Phoenix.LiveView, ~r/\.Web$/]</code>)</li>
<li><code>Credo.Check.Warning.UnusedOperation</code> update
<code>:modules</code> param: instead of a list of functions to check,
<code>:all</code> can be given to check all functions in a module</li>
<li>New Check:
<code>Credo.Check.Refactor.CondInsteadOfIfElse</code></li>
<li>New Check: <code>Credo.Check.Warning.WrongTestFilename</code></li>
</ul>
<h2>1.7.16</h2>
<ul>
<li>Fix compatibility &amp; compiler warnings with Elixir
1.20.0-rc.1</li>
<li><code>Credo.Check.Refactor.PassAsyncInTestCases</code> add new param
<code>:force_comment_on_explicit_false</code> (defaults to
<code>false</code>)</li>
<li><code>Credo.Check.Warning.Dbg</code> add new param
<code>:allow_captures</code> (defaults to <code>false</code>)</li>
<li>New Check: <code>Credo.Check.Warning.UnusedMapOperation</code></li>
<li>New Check: <code>Credo.Check.Warning.UnusedOperation</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rrrene/credo/commit/068fcc7bfb3d35ed35a000bd2a9a02b637114119"><code>068fcc7</code></a>
Bump version to 1.7.17</li>
<li><a
href="https://github.com/rrrene/credo/commit/5eccc0d146291867ec6f1e22425159484185b3f8"><code>5eccc0d</code></a>
Update CHANGELOG</li>
<li><a
href="https://github.com/rrrene/credo/commit/0b087207d614db8c31fd3aac0f659c49d750a382"><code>0b08720</code></a>
Update .credo.exs</li>
<li><a
href="https://github.com/rrrene/credo/commit/122a258459418e4ca1f9b1cffbc78b11be870a3e"><code>122a258</code></a>
Add :excludes to .formatter.exs</li>
<li><a
href="https://github.com/rrrene/credo/commit/09960e67678f5fc8e7e06d715eb3d94702d48590"><code>09960e6</code></a>
Add defaults to :ignore_modules_using in ModuleDoc</li>
<li><a
href="https://github.com/rrrene/credo/commit/472f12e222abed650d95c8b662836b68da114836"><code>472f12e</code></a>
Add repro for <a
href="https://github.com/rrrene/credo/issues/1235">#1235</a> to
prevent regressions</li>
<li><a
href="https://github.com/rrrene/credo/commit/11235d5754a1c93eb321ce2b2e989ed8601c0a0d"><code>11235d5</code></a>
Merge branch '1254-module-doc-new-param'</li>
<li><a
href="https://github.com/rrrene/credo/commit/e73ea15e05a2d2a14083a0b1567b021d9acfb084"><code>e73ea15</code></a>
Integrate new param for ModuleDoc</li>
<li><a
href="https://github.com/rrrene/credo/commit/0cfeba513f76b074a9be9913003115b9df9ada60"><code>0cfeba5</code></a>
Update check id for WrongTestFilename</li>
<li><a
href="https://github.com/rrrene/credo/commit/04cb8633f9bff15557f062b1ca243a962eb53c8c"><code>04cb863</code></a>
Merge branch '1258-new-test-files-check'</li>
<li>Additional commits viewable in <a
href="https://github.com/rrrene/credo/compare/v1.7.15...v1.7.17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=credo&package-manager=hex&previous-version=1.7.15&new-version=1.7.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…andlers (zoedsoupe#109)

## Problem

After adding Anubis 0.17.1 I noticed that memory usage of my Phoenix
application slowly climbed over the hours until it ran out of memory and
died. It's a pre-production app with little usage and when that happened
there were just logs to connect to the MCP server.

## Solution

After adding some tracking, GPT-5.3-Codex diagnosed this as follows:

- In streamable HTTP transport, SSE handlers are keyed by session_id.
- Reconnects on the same `session_id` can overwrite map entries, and old
stream processes can become orphaned (no longer in `sse_handlers`).
- Orphaned stream loops sit in receive forever.
- `expiry_timers` map in Anubis server is not cleaned on
`:session_expired` path.

With this fix:

- adding pid-aware `unregister_sse_handler/3` so only the expected
handler can remove itself
- replacing existing handlers safely on register (demonitor old ref,
close old handler, monitor/store the new handler)
- updating plug on-close and stale-handler cleanup paths to pass the
handler pid

## Rationale

I've tested this and deployed it to my app, and memory usage has been
stable the last few hours.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Enhanced Server-Sent Events (SSE) handler registration logic to
properly manage concurrent handler instances for the same session,
preventing race conditions and ensuring consistent handler state.

* **Tests**
* Added test case to verify SSE handler lifecycle behavior when multiple
handlers are registered for the same session.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem

I have an Elixir app that provides an MCP server and I wanted to utilize
the new MCP Apps extension
(https://modelcontextprotocol.io/extensions/apps/overview). To do this,
I needed support for adding the _meta field to tool declarations so
clients know which visual resource to render alongside a tool's output.

## Solution

I added a meta field to the Tool struct and wired it through the JSON
encoder so _meta appears in the serialized MCP tool object. Tools can
now declare `meta: %{"ui" => %{"resourceUri" =>
"ui://my-app/dashboard"}}` via `use Anubis.Server.Component`.

## Rationale

This is the minimal change needed to support the MCP spec's _meta field
— I only touched the Tool struct and its serialization path.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Tool components now support optional metadata that can be provided
during tool definition and is automatically included in exported tool
information and JSON responses.

* **Tests**
* Added comprehensive tests validating metadata functionality, including
JSON encoding and callback optionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Problem

Warning `Session store enabled but adapter not available` when the
session store
is not enabled.

## Solution

Properly check the config's `enabled` boolean param

## Rationale




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Refactored session store initialization to improve configuration
handling and logging.
* **Tests**
* Added test coverage for session store configuration scenarios and
related logging.
* **Note**
  * No user-facing changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
dependabot Bot and others added 14 commits July 15, 2026 14:47
Bumps [cowboy](https://github.com/ninenines/cowboy) from 2.16.1 to
2.17.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ninenines/cowboy/commit/3de77f8f08f57d6a6d5f0e646b7e8ecd4ccd525b"><code>3de77f8</code></a>
Cowboy 2.17.0</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/98d97c02f651c67247142ee59d68f697df07c7c4"><code>98d97c0</code></a>
Clarify cowboy_decompress_h behavior in manual</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/19f3a5bad5342601cd4a28f5976adb825150b84e"><code>19f3a5b</code></a>
HTTP/1: Skip request trailers if any</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/c8cd061c509c5b63ee8bbc4c2fb4fcab1e8215d0"><code>c8cd061</code></a>
Warn about cowboy_compress_h in the security checklist</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/ad44cbf0a9052849652fbcf049494d6514cae8b8"><code>ad44cbf</code></a>
HTTP/1: Fix rejecting header lines with missing colon</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/3cab5a3a4ee14a835e8beb581dbb7cdeb54b45a6"><code>3cab5a3</code></a>
Add max_keys option to cowboy_req qs functions</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/6e3609c38dc3d5b1aa6b420ededaefa98963f1a3"><code>6e3609c</code></a>
Clarify static handler behavior with symlinks</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/c19a177276ae14afdb73bf60977263008ece0587"><code>c19a177</code></a>
Don't use 'catch' in tests</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/a8541a2d74c561a320d474c50af7d6c0658ba9a0"><code>a8541a2</code></a>
Temporarily use Cowlib master</li>
<li><a
href="https://github.com/ninenines/cowboy/commit/db5cc07249eeba831a2f1aec992c6bbd6ab25e43"><code>db5cc07</code></a>
Extend invalid_response_headers to early_error responses</li>
<li>Additional commits viewable in <a
href="https://github.com/ninenines/cowboy/compare/2.16.1...2.17.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cowboy&package-manager=hex&previous-version=2.16.1&new-version=2.17.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [plug](https://github.com/elixir-plug/plug) from 1.20.1 to 1.20.3.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/elixir-plug/plug/blob/main/CHANGELOG.md">plug's
changelog</a>.</em></p>
<blockquote>
<h2>v1.20.3 (2026-07-09)</h2>
<h3>Security</h3>
<ul>
<li>[Plug.Parsers.MULTIPART] Count files and skipped multipart parts
towards the length limit (CVE-2026-56814)</li>
<li>[Plug.Conn.Cookies] Raise if <code>;</code> is present in cookie
attributes (CVE-2026-56813)</li>
</ul>
<h2>v1.20.2 (2026-06-30)</h2>
<h3>Bug fixes</h3>
<ul>
<li>[Plug.Conn] Set state to <code>:set_upgrade</code> and status to 101
when running <code>before_send</code> callbacks for upgrades</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/elixir-plug/plug/commit/9fa11c8ebedbe68531eba25d8f81b9282e0514da"><code>9fa11c8</code></a>
Release v1.20.3</li>
<li><a
href="https://github.com/elixir-plug/plug/commit/eceb8315ce9a31ef784943a95a8624ebd1bc7e06"><code>eceb831</code></a>
Raise if ; is present in cookie attributes</li>
<li><a
href="https://github.com/elixir-plug/plug/commit/56edca2ce35fe5589cd581644d8a4493fa5f484e"><code>56edca2</code></a>
Count files and skipped multiparts towards length</li>
<li><a
href="https://github.com/elixir-plug/plug/commit/cf15f0a5d9ce4390adff1cb4071a44013793ddd4"><code>cf15f0a</code></a>
Release v1.20.2</li>
<li><a
href="https://github.com/elixir-plug/plug/commit/5d0fe881537531c760d7e4a54bb93fa9d346ac4c"><code>5d0fe88</code></a>
Fix register_before_send with upgrade_adapter (<a
href="https://github.com/elixir-plug/plug/issues/1320">#1320</a>)</li>
<li>See full diff in <a
href="https://github.com/elixir-plug/plug/compare/v1.20.1...v1.20.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=plug&package-manager=hex&previous-version=1.20.1&new-version=1.20.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…stion DoS (zoedsoupe#188)

## Summary

`Anubis.Server.Registry.resolve_session_name/3` named each session
process by interpolating the session id into an atom. For HTTP
transports the session id comes from the client-supplied
`mcp-session-id` header, and atoms are never garbage collected, so a
client sending many distinct session ids grows the global atom table
without bound until the BEAM hits its atom limit (default ~1,048,576)
and the node crashes with `system_limit`. That is a remote
denial-of-service reachable in a default production deployment.

This PR routes the default session naming through an Elixir `Registry`
(`:via` tuple keyed by the session-id string) so the client-supplied id
stays ordinary term data and no atom is minted per session.

## The vulnerability

Two code paths built a fresh atom per session id:

- The fallback in `resolve_session_name/3`, used when a registry adapter
does not implement the optional `session_name/2` callback
(`lib/anubis/server/registry.ex`):

  ```elixir
defp session_name_from_registry_name(registry_name, session_id) when
is_atom(registry_name) do
    :"#{registry_name}.session.#{session_id}"
  end
  ```

- The shipped `Registry.Local` adapter
(`lib/anubis/server/registry/local.ex`), the default for HTTP
transports, which implemented `session_name/2` the same way:

  ```elixir
def session_name(registry_name, session_id), do:
:"#{registry_name}.session.#{session_id}"
  ```

`Registry.PG` does not implement `session_name/2`, so it hits the
atom-minting fallback. Both shipped HTTP-capable registries are
therefore affected — no custom adapter required. STDIO is unaffected
because it uses a single, fixed `"stdio"` session id.

The name is used only as the `name:` passed to `start_session/2`; after
start, lookups go through the adapter's `lookup_session/2` (ETS or
`:pg`) by pid. So the atom only ever served as the process registration
name, which makes it safe to replace with a `:via` name.

## The fix

`resolve_session_name/3` now returns:

```elixir
{:via, Registry, {naming_registry_name(registry_name), session_id}}
```

- A per-server Elixir `Registry` (`keys: :unique`) is started in the
HTTP supervision tree (`build_http_children/8`). Its name is derived
from the compile-time bounded `registry_name` via the new
`Registry.naming_registry_name/1`, so it does not itself mint
per-session atoms.
- The client-supplied `session_id` is the registry key (binary term
data), never converted to an atom.
- `:via` tuples are drop-in replacements for atom names in
`GenServer.start_link/3` / `start_session/2`, and they preserve the
existing `{:error, {:already_started, pid}}` semantics that `plug.ex`
and `sse.ex` rely on for concurrent requests to the same session.
- The optional `session_name/2` callback is kept for adapters that want
their own `:via` naming (e.g. Horde). Only the shipped atom-minting
`Registry.Local.session_name/2` is removed, so it falls through to the
safe default.

Internal, compile-time bounded names (transports, supervisors, task
stores) keep their atom naming — they are not influenced by client
input. The public `Registry.session_name/2` helper still builds an atom
and is now documented as test-only / trusted-id-only.

## Changes

- `lib/anubis/server/registry.ex` — `resolve_session_name/3` fallback
now returns a `:via` `Registry` tuple; added `naming_registry_name/1`;
documented why session names must not be atoms and marked
`session_name/2` as trusted-id-only.
- `lib/anubis/server/supervisor.ex` — start the per-server naming
`Registry` in `build_http_children/8`.
- `lib/anubis/server/registry/local.ex` — removed the atom-minting
`session_name/2`; uses the safe default.
- `test/anubis/server/registry_test.exs` — regression test asserting
`:erlang.system_info(:atom_count)` stays stable across 50,000 distinct
session ids, plus round-trip tests that a process is reachable by its
resolved `:via` name and that duplicate starts return
`{:already_started, pid}`.
- `test/anubis/server/transport/streamable_http/plug_test.exs` — start
the naming `Registry` in the session-handling setup (mirrors production
wiring).

## Verification

```
mix test           # registry/transport/session suites: 63 tests, 0 failures
mix format --check-formatted
mix credo          # no issues on changed files
```

The regression test fails against the previous atom-based naming and
passes with the fix.

---------

Co-authored-by: zoey <zoey.spessanha@zeetech.io>
…e#198)

## Problem

On Streamable HTTP, the client sends `notifications/initialized` and its
first request (e.g. `tools/list`) as two separate, near-simultaneous
HTTP requests. The client sends them in order, but the transport doesn't
guarantee order, so `tools/list` sometimes lands before the
notification.

When it does, the session rejects it with `"Server not initialized"`
([gate](https://github.com/zoedsoupe/anubis-mcp/blob/main/lib/anubis/server/session.ex#L603-L605),
[error](https://github.com/zoedsoupe/anubis-mcp/blob/main/lib/anubis/server/session.ex#L635-L645)).
A client that doesn't retry then stalls. In my case, the Claude Desktop
client hangs ~30s, then sends `notifications/cancelled`.

The spec lets a client send requests as soon as the server responds to
`initialize`, so this is stricter than the spec requires.

## Solution

Mark the session initialized on the `initialize` response, not only in
the `notifications/initialized` handler. Adds a test for a request
arriving after `initialize` but before the notification.

## Rationale

Matches the official SDKs, which mark the session ready on the
`initialize` response:

- TypeScript
([streamableHttp.ts#L501](https://github.com/modelcontextprotocol/typescript-sdk/blob/caa25503cdfc449d116c204e866bccc2617d7037/src/server/streamableHttp.ts#L501))
- Python
([#1478](modelcontextprotocol/python-sdk#1478))
- Rust
([#788](modelcontextprotocol/rust-sdk#788))

The spec says **SHOULD NOT**, not **MUST NOT**, so the notification
isn't a hard gate.
…ssion auto-recovery (zoedsoupe#208)

**Addresses behaviour 1 in zoedsoupe#204.** **Follows up zoedsoupe#125**, which added
transparent
auto-recovery so a client caching a stale session id keeps working
instead of
erroring out. This completes that path for servers that authenticate
per-request.

### Problem

On the StreamableHTTP transport, when a non-`initialize` request arrives
for an
expired/unknown session, the recovery path (`find_or_create_session` ->
`start_and_auto_initialize_session` -> `Session.auto_initialize/1`) runs
the
recovered `init/2` (and the optional `handle_session_expired/2`) with an
**empty
`frame.assigns` and empty `frame.context`**. `auto_initialize/1` takes
no
transport context, so the triggering request's `conn.assigns`, headers,
remote
IP, and auth claims are dropped.

`Anubis.Server.Frame`'s moduledoc says assigns inherit from
`Plug.Conn.assigns`
for HTTP transports. That holds on the normal request path but not on
recovery:
a server that authenticates in a Plug (setting
`conn.assigns.current_user`) and
reads it back in `init/2` sees an *unauthenticated* frame on any request
that
triggers recovery, even though that request carried valid credentials.

### Change

- Add `Session.auto_initialize/2 (session, transport_context)`; keep
  `auto_initialize/1` delegating with `nil` (backwards-compatible).
- Thread the request's transport context down the plug's unknown-session
recovery branch into `auto_initialize/2`. Only that branch is affected;
  `initialize` and existing-session branches are unchanged. The
notification/response paths (which already return 404 for unknown
sessions) are
  untouched.
- In the recovery handler, apply the live request's assigns to the
recovered
  frame and pass the context to `prepare_frame/2` so `frame.context`
(headers/remote_ip/auth) is populated. Both `handle_session_expired/2`
and
`init/2` now see the populated frame, consistent with the normal request
path.

### Rationale for replace assigns instead of merging

On the recovery path the live request's assigns **replace** the frame's
assigns
(only when the request actually carries assigns. An empty/absent set
leaves any
store-restored assigns untouched). This is deliberate rather than a
merge:

The session store round-trips assigns through serialization -
`Frame.to_saved`/
`from_saved` do no key normalization - so a JSON-backed store returns
assigns
with **string keys** (`"current_user"`) while live `conn.assigns` are
**atom
keys** (`:current_user`). A key-wise merge would leave *both* present: a
host
reading the atom key gets the live value, but a stale string-keyed value
silently
survives alongside it, leaving a latent auth bug. Atom-normalizing
untrusted stored
strings is an atom-table-exhaustion risk, so the recovery path treats
the live
per-request assigns as authoritative and replaces rather than
reconciling.

### Tests

- Recovered frame carries live request assigns (no store).
- Recovered `frame.context` carries headers / remote_ip / auth.
- Live assigns replace stale store assigns (the string-vs-atom key case
above);
  the stale key does not survive.
- Store-only recovery preserved when the request carries no assigns.
- `auto_initialize/1` backwards-compat unchanged.

Verified end-to-end by driving a `conn` with `conn.assigns` set through
the plug
to the recovery branch and confirming the recovered session's frame
carries them.
`mix lint` (format + credo --strict + dialyzer) and `mix test` pass.

---------

Co-authored-by: zoey <zoey.spessanha@zeetech.io>
…oedsoupe#209)

## Summary

Enable `Process.flag(:trap_exit, true)` in `Session.init/1` so explicit
supervisor shutdown (e.g. StreamableHTTP client `DELETE`) runs
`terminate/2` and emits `[:anubis_mcp, :server, :terminate]` telemetry.

## Motivation

`Anubis.Server.Session` did not trap exits while the transport processes
(`streamable_http`, `sse`, `stdio`) already do. When a session is
stopped via `DynamicSupervisor.terminate_child/2` (the DELETE path), OTP
delivers a `:shutdown` exit to a non-trapping GenServer, which
terminates immediately **without** calling `terminate/2`.

That silently skips:
- library `[:anubis_mcp, :server, :terminate]` telemetry
- optional server-module `terminate/2` cleanup hooks

Idle-expiry shutdown already worked because it uses callback-initiated
`{:stop, ...}`.

Fixes the outstanding part 4 of zoedsoupe#204 (session idle expiry and assigns
recovery are tracked separately in zoedsoupe#208).

## Changes

- `lib/anubis/server/session.ex`: set `trap_exit` at the top of `init/1`
- `test/anubis/server/session_test.exs`: regression test stopping a
session via `DynamicSupervisor.terminate_child/2` and asserting
terminate telemetry

## Tests

- `mix test test/anubis/server/session_test.exs` — 21 tests, 0 failures
- `mix test test/anubis/server/session_test.exs:537` — new regression
test passes

## Notes

Session task work already uses `Task.Supervisor.async_nolink/2` +
monitors, matching the existing transport `trap_exit` pattern. No new
`handle_info` for `{:EXIT, ...}` is required.

---------

Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
Co-authored-by: zoey <zoey.spessanha@zeetech.io>
zoedsoupe#210)

Addresses behaviour 4 in zoedsoupe#204.

### Problem

`Anubis.Server.Session` is `use GenServer` and its `init/1` returns
`{:ok, state, :hibernate}` without `Process.flag(:trap_exit, true)`. The
library
already traps exits on its transport processes
(`server/transport/streamable_http.ex`, `server/transport/stdio.ex`),
just not on
`Session`.

Because of that, when a session is stopped via the supervisor —
`Supervisor.stop_session/3` -> `DynamicSupervisor.terminate_child/2`, a
`:shutdown`
exit — the non-trapping process dies immediately without running
`terminate/2` (standard OTP: a non-trapping process receiving
`:shutdown`
terminates without invoking `terminate/2`).

This is the path a spec-compliant client `DELETE /mcp` takes
(`handle_delete -> delete_session_from_store -> stop_session_process ->
Supervisor.stop_session`). On it:

- the library's own `[:anubis_mcp, :server, :terminate]` telemetry
(emitted before
  the `exported?(module, :terminate, 2)` gate) never fires;
- the server module's optional `terminate/2` never fires — so any
cleanup/observability a host hangs off `terminate/2` is silently skipped
on
  explicit disconnect.

The idle-expiry path already works, because
`handle_info(:session_expired, ...)`
returns `{:stop, {:shutdown, :session_expired}, ...}`, and a
callback-initiated
`{:stop, …}` always runs `terminate/2`. The gap is specifically the
explicit-stop (DELETE) path — arguably the more common close for
short-lived tool
sessions.

### Change

- `Process.flag(:trap_exit, true)` in `Session.init/1`. With trapping,
the
supervisor's `:shutdown` is delivered through the `gen_server` loop and
`terminate/2` (+ the `[:anubis_mcp, :server, :terminate]` telemetry)
runs on
every stop path — explicit DELETE, idle expiry, and supervisor shutdown
alike —
  consistent with how the transports already behave.
- An explicit `handle_info({:EXIT, _pid, _reason}, state)` clause that
ignores the
signal. Session's general `handle_info` catch-all forwards unmatched
messages to
the host's `module.handle_info/2`; this clause keeps a stray `{:EXIT,
…}` (from
a process a host links to the session pid) from leaking into the host
callback.

### Trapping rationale

Trapping was deemed safe due to:

- Tasks are spawned with `Task.Supervisor.async_nolink`, so they are not
linked to
the session — task crashes deliver `{:DOWN, …}` (already handled) /
`{ref,
  result}`, never `{:EXIT, …}`. Trapping changes nothing for tasks.
- The session links only to its supervisor parent; a parent exit signal
to a
trapping GenServer is intercepted by the `gen_server` loop and triggers
normal
  termination (which runs `terminate/2`) rather than being dispatched to
  `handle_info`. That interception is exactly what makes the fix work.
- So in normal operation the session receives no `{:EXIT, …}` at
`handle_info`; the
  explicit clause is defense-in-depth for the exotic host-link case.

### Tests

- Host `terminate/2` fires when the session is stopped via the
supervisor
(`DynamicSupervisor.terminate_child`, the mechanism
`Supervisor.stop_session/3`
  uses), asserted via telemetry; the process is confirmed dead.
- The library `[:anubis_mcp, :server, :terminate]` telemetry fires on
the same
  path, with the session id in metadata.

Both fail before the change (the session dies on `:shutdown` without
running
`terminate/2`) and pass after. `mix lint` (format + credo --strict +
dialyzer)
and `mix test` pass.

---------

Co-authored-by: zoey <zoey.spessanha@zeetech.io>
…st (follow-up to zoedsoupe#180) (zoedsoupe#213)

**Follow-up to zoedsoupe#180 (issue zoedsoupe#177).** zoedsoupe#180 fixed the client transport
dropping the configured `:headers` (e.g. `Authorization`) on the SSE
**GET** request. The same bug is still present on the **DELETE**
session-teardown leg — this completes the pair.

### Problem

`Anubis.Transport.StreamableHTTP.delete_session/1` builds its DELETE
request headers from a bare `%{}`:

```elixir
defp delete_session(state) do
  headers = put_session_header(%{}, state.session_id)
  ...
```

So a spec-compliant client `DELETE /mcp` reaches the server without the
configured headers — an authenticated client's session close arrives
**unauthenticated**. A server that authorizes the DELETE (or just logs
the caller) sees no `Authorization`.

This is the exact shape of the bug zoedsoupe#180 already fixed for the SSE GET.
Every other request leg bases its headers on `state.headers`:

- POST — `send_http_request/3`: `state.headers |> Map.put("accept", ...)
|> ...`
- SSE GET — `build_sse_headers/1` (the zoedsoupe#180 fix): `state.headers |>
Map.put("accept", "text/event-stream") |> ...`

DELETE was the one remaining leg still starting from `%{}`. zoedsoupe#180 fixed
GET but didn't touch DELETE; this is the natural completion of that
change.

### Solution

Base the DELETE headers on `state.headers`, exactly like the POST and
GET legs:

```elixir
-  headers = put_session_header(%{}, state.session_id)
+  headers = put_session_header(state.headers, state.session_id)
```

`put_session_header/2` just adds the session id (or no-ops when absent),
so this preserves all configured headers and still attaches
`mcp-session-id`. `state.headers` defaults to `%{}` when none are
configured, so the no-headers case is unchanged.

DELETE carries no body and expects none, so it correctly does **not**
take the `accept`/`content-type` headers that `build_sse_headers`/the
POST path add — only the configured headers plus the session id.

### Test

Added alongside the zoedsoupe#180 GET-headers test in the "headers and options"
describe: establish a session (a POST that returns `mcp-session-id`),
then `shutdown/1` (which issues the DELETE), and assert the DELETE
carried the configured `Authorization` header. Fails before the change
(the header arrives as `nil`) and passes after. `mix lint` and `mix
test` pass.

Co-authored-by: zoey <zoey.spessanha@zeetech.io>
…edsoupe#218)

## Problem

The Streamable HTTP transport can push a message to a single session's
SSE stream (`route_to_session/3`) or broadcast to every connected
handler (`send_message/3`), but there is no way to deliver to a
*selected subset* of connected subscribers, and no way for a host to
attach application context to a subscriber or observe connected-handler
counts. This is the single-node building block behind the cross-node
delivery discussed in zoedsoupe#189 / zoedsoupe#190.

## Solution

Add opaque per-subscriber metadata plus selector-based delivery:

- `register_sse_handler/3` stores an opaque `metadata` map verbatim; the
2-arity form delegates with `%{}`. The transport never interprets the
map. Handlers stay keyed by `session_id`, so `get`/`route`/`unregister`
remain O(1); the stored value widens from `{pid, ref}` to `{pid, ref,
metadata}`.
- `handler_count/1` (total) and `handler_count/2` (a predicate over
metadata) expose connected-handler gauges.
- `send_message_to_subscribers/4` fans a message out to every handler
whose metadata satisfies a caller-supplied selector -- filling the gap
between `route_to_session/3` (one session) and `send_message/3` (all
handlers).
- The Plug gains a `:subscriber_metadata` option -- a `(Plug.Conn.t() ->
map())` callback invoked when an SSE stream opens -- so hosts can tag
subscribers from the request (tenant, user, feature scope). Defaults to
`%{}`, so existing behavior is unchanged.

## Rationale

Server-initiated delivery to an application-defined subset of
subscribers is a general MCP-server need (see zoedsoupe#189 / zoedsoupe#190). The library
cannot know a host's routing dimensions, so metadata is kept fully
opaque and host-populated via the plug callback; the transport provides
only storage + selector primitives. Keeping the `session_id` key (rather
than re-keying by `{session_id, pid}`) preserves the existing O(1)
lookups. This is not spec-mandated -- it is a capability extension
layered on the existing per-session SSE registry -- and I'm happy to
adjust the shape (e.g. the callback signature) to your preference.

## Tests

- Transport: `register_sse_handler/3` metadata round-trip +
`handler_count/1,2`; 2-arg default of `%{}`;
`send_message_to_subscribers/4` delivers only to matching subscribers.
- Plug: `init/1` stores and defaults the `:subscriber_metadata`
callback; end-to-end GET SSE registration attaches the configured
metadata (verified via `handler_count/2`).


Co-authored-by: zoey <zoey.spessanha@zeetech.io>
…econnect flap (zoedsoupe#215)

## Problem

When a new GET/SSE stream registers for a session that already has a
live SSE handler, the Streamable HTTP transport demonitors the incumbent
and sends it `:close_sse`. A spec-compliant MCP client holding a
standalone GET stream (e.g. via the MCP SDK / `mcp-remote`) treats a
server-initiated close as a signal to immediately reconnect. That
reconnect races the next registration and produces an unbounded
register/close flap (`sse_handler_registered` / `sse_closing` churn).
During each no-handler window, server-to-client notifications broadcast
via `send_message` are silently dropped, because there is no registered
handler to receive them.

## Solution

Keep the takeover semantics (the newly registered handler becomes the
active handler for the session) but stop proactively closing the
superseded handler in `handle_call({:register_sse_handler, ...})`. The
two prior branches collapse into one that only calls
`Process.demonitor(old_ref, [:flush])`. The superseded handler is reaped
by its own connection lifecycle: `Anubis.SSE.Streaming.start/4` wraps
the receive loop in `try/after on_close.()`, and the Plug installs
`on_close: unregister_sse_handler(transport, session_id, handler_pid)`.
The `expected_pid` guard added in zoedsoupe#109 ensures the superseded handler's
eventual unregister cannot evict the handler that took over.

## Rationale

MCP Streamable HTTP permits a client to hold a standalone GET/SSE stream
for server-initiated messages, and a server-initiated close is a
legitimate reconnect trigger for the client -- which is exactly why
proactively closing on takeover produced the flap. Letting the old
connection close on its own lifecycle preserves takeover without the
churn.

## Tests

Adds a transport test: two handlers register on the same session; the
second becomes the active handler (asserted via `get_sse_handler`), and
the first receives no `:close_sse` (`refute_receive`). Existing
transport suite unchanged.

Co-authored-by: zoey <zoey.spessanha@zeetech.io>
@zoedsoupe

Copy link
Copy Markdown
Owner

hey, do you mind to solve ci issues + open comments?

@zoedsoupe

Copy link
Copy Markdown
Owner

hey, i've messed with the history but i've fixed and manually merged your contribution. sorry for this mess up and thanks again for your contribution!!

@zoedsoupe zoedsoupe closed this Jul 16, 2026
@zoedsoupe zoedsoupe mentioned this pull request Jul 16, 2026
zoedsoupe added a commit that referenced this pull request Jul 16, 2026
🚀 Want to release this?
---


##
[1.9.0](v1.8.0...v1.9.0)
(2026-07-16)


### Features

* **streamable_http:** add spec resumability (Last-Event-ID replay)
([#216](#216))
([78e33b4](78e33b4))
* support pre_initialized sessions for cross-pod restore
([#187](#187))
([13be0d7](13be0d7))


### Bug Fixes

* **session:** return encodable JSON-RPC errors when init/2 fails
([#211](#211))
([f9af7cc](f9af7cc))
* **streamable_http:** emit telemetry on SSE handler registration
([#217](#217))
([4a4c528](4a4c528))
* **streamable_http:** restore session from store on notif/resp registry
miss ([#221](#221))
([d757b39](d757b39))
* **streamable_http:** return correct JSON-RPC error codes for parse
failures ([#222](#222))
([1867994](1867994))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
@BobbieBarker

Copy link
Copy Markdown
Contributor Author

hey, i've messed with the history but i've fixed and manually merged your contribution. sorry for this mess up and thanks again for your contribution!!

No worries and thank you for getting this merged into the project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.