An extension adds negotiated behavior without changing the core protocol
profile. It implements Snodo.Extension, declares exact-versioned
Snodo.Extension.Method values, and is installed on the server:
use Snodo.Server,
name: "my-server",
version: "1.0.0",
extensions: [MyApp.Extension]Runtime construction rejects duplicate extension IDs, methods that collide with any core method (including unsupported and MRTR-only ones), and collisions between extensions. An installed route runs only when both peers advertise the extension and its callback accepts the negotiation. An advertised, compatible extension may also wrap core dispatch and contribute HTTP policy through generic hooks. An installed but unadvertised extension does nothing.
Middleware that stores or defers a call instead of passing it on calls
Snodo.Extension.check_dispatch/3 first, so the deferred work passes the same
lookup, authorization, and argument checks as a direct call.
See examples/06_custom_extension.exs.
Declared extensions
Some extensions add no methods: the server advertises them, reads what the
client declared, and attaches the extension's _meta to tools, resources, or
results. Declare one with its server settings instead of writing a module:
use Snodo.Server,
name: "my-server",
version: "1.0.0",
extensions: [{:declared, "com.example/charts", %{"version" => "1"}}]The runtime advertises the id under capabilities["extensions"] with those
settings; an entry you put there yourself wins. In a handler,
Snodo.Context.extension_settings/2 returns the settings the client sent for
the id, or nil when the client did not declare it, so the handler can fall
back to plain content:
tool "sales_chart" do
@impl true
def call(_arguments, context) do
case Snodo.Context.extension_settings(context, "com.example/charts") do
nil ->
{:ok, "Q3 sales: 120, 140, 180"}
_client_settings ->
{:ok,
Snodo.Result.text("Q3 sales: 120, 140, 180",
metadata: %{"com.example/charts" => %{"kind" => "bar", "series" => [120, 140, 180]}}
)}
end
end
endFor a published extension such as MCP Apps (io.modelcontextprotocol/ui),
attach the _meta keys its specification defines. A declared extension has no
routes, middleware, or negotiation callback; write a Snodo.Extension module
when you need any of those.
Tasks
snodo_tasks implements the io.modelcontextprotocol/tasks extension on those
hooks. It augments tools/call, keeps tasks/get, tasks/update, and
tasks/cancel outside the core catalog, and adds taskIds filters and
notifications/tasks to subscriptions.
The application owns the store and runner. The package includes memory and
DETS stores; snodo_tasks_postgres and snodo_tasks_sqlite add
transactional stores behind an application-owned Ecto Repo. Recovery is
at-least-once, so work must use its stable idempotency key for external
effects.
- Tasks package, including the stress-testing harness
- Writing a Tasks store
- PostgreSQL store
- SQLite store
Examples 07, 08, 09, 17, and 26 run from extensions/tasks; example 10 from
extensions/tasks_postgres with a live database; example 11 from
extensions/tasks_sqlite.