# snodo

[![Compatibility](https://github.com/joshrotenberg/snodo/actions/workflows/compatibility.yml/badge.svg)](https://github.com/joshrotenberg/snodo/actions/workflows/compatibility.yml)
[![Protocol regression](https://github.com/joshrotenberg/snodo/actions/workflows/protocol.yml/badge.svg)](https://github.com/joshrotenberg/snodo/actions/workflows/protocol.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/snodo.svg)](https://hex.pm/packages/snodo)
[![Docs](https://img.shields.io/badge/hexdocs-docs-purple.svg)](https://hexdocs.pm/snodo)
![Elixir](https://img.shields.io/badge/Elixir-1.18%2B-blueviolet)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/joshrotenberg/snodo/blob/main/LICENSE)

An Elixir library for building [Model Context Protocol](https://modelcontextprotocol.io)
servers and clients. It speaks MCP `2026-07-28`, with opt-in support for
initialize-era clients (`2025-11-25` and `2025-06-18`) over HTTP and stdio.
The client speaks all three versions and negotiates one when it connects.

`snodo` is at 0.x: the API may change between minor versions until 1.0.

- **Servers** from inline blocks or ordinary modules, served over stdio, a
  built-in Streamable HTTP listener, or Plug and Bandit.
- **A client** that calls any MCP server in process, over stdio, or over HTTP.
- **The 2026-07-28 surface:** discovery, tools, resources and templates,
  prompts, completion, pagination, `subscriptions/listen`, progress,
  cancellation, and multi round-trip requests with elicitation.
- **No runtime dependencies** in the core: it uses Elixir's built-in `JSON`
  and OTP. Optional sibling packages add Tasks, Plug, OAuth 2.1 resource
  server support, full JSON Schema validation, and `:telemetry` events.
  An optional proxy package fronts multiple MCP backends.

## Packages

| Package | Adds | Docs |
|---|---|---|
| [`snodo`](https://hex.pm/packages/snodo) | Protocol core, router, server DSL, client, stdio and HTTP transports | [HexDocs](https://hexdocs.pm/snodo) |
| [`snodo_plug`](https://hex.pm/packages/snodo_plug) | `Snodo.Transport.Plug` for Plug and Bandit applications | [HexDocs](https://hexdocs.pm/snodo_plug) |
| [`snodo_jsv`](https://hex.pm/packages/snodo_jsv) | Full JSON Schema 2020-12 validation through JSV | [HexDocs](https://hexdocs.pm/snodo_jsv) |
| [`snodo_oauth`](https://hex.pm/packages/snodo_oauth) | OAuth 2.1 resource server plugs (protected resource metadata, bearer token verification, scope policy) and the client authorization flows for `Snodo.Client` | [HexDocs](https://hexdocs.pm/snodo_oauth) |
| [`snodo_telemetry`](https://hex.pm/packages/snodo_telemetry) | `Snodo.Instrumentation.Telemetry`, an instrumentation sink that emits `:telemetry` events | [HexDocs](https://hexdocs.pm/snodo_telemetry) |
| [`snodo_proxy`](https://hex.pm/packages/snodo_proxy) | Aggregating proxy for MCP backends | [HexDocs](https://hexdocs.pm/snodo_proxy) |
| [`snodo_tasks`](https://hex.pm/packages/snodo_tasks) | The `io.modelcontextprotocol/tasks` extension with an application-owned store and runner | [HexDocs](https://hexdocs.pm/snodo_tasks) |
| [`snodo_tasks_postgres`](https://hex.pm/packages/snodo_tasks_postgres) | PostgreSQL store for Tasks | [HexDocs](https://hexdocs.pm/snodo_tasks_postgres) |
| [`snodo_tasks_sqlite`](https://hex.pm/packages/snodo_tasks_sqlite) | SQLite store for Tasks | [HexDocs](https://hexdocs.pm/snodo_tasks_sqlite) |

Add the packages you need to `mix.exs`. Each sibling brings `snodo` with it:

<!-- x-release-please-start-version -->
```elixir
def deps do
  [
    {:snodo, "~> 0.4.1"},
    {:snodo_plug, "~> 0.4.1"}
  ]
end
```
<!-- x-release-please-end -->

Elixir 1.18 or later is required. The sibling packages live in this repository
under `integrations/` and `extensions/`.

## Quick start

A server with one tool, one resource template, and one prompt:

```elixir
defmodule Greeter do
  use Snodo.Server, name: "greeter", version: "0.1.0"

  tool "greet", description: "Create a greeting" do
    argument "name", :string, required: true

    @impl true
    def call(%{"name" => name}, _context), do: {:ok, "Hello, #{name}!"}
  end

  resource "profile", uri_template: "people://{name}/profile", mime_type: "application/json" do
    @impl true
    def read(%{"name" => name}, _context), do: {:ok, %{"name" => name}}
  end

  prompt "introduce", description: "Introduce someone" do
    argument "name", required: true

    @impl true
    def render(%{"name" => name}, _context), do: {:ok, "Introduce #{name} in one sentence."}
  end
end
```

Call it in process with `Snodo.Client`:

```elixir
{:ok, client} = Snodo.Client.direct(Greeter.runtime())

{:ok, [%{"name" => "greet"}]} = Snodo.Client.list_tools(client)
{:ok, result} = Snodo.Client.call_tool(client, "greet", %{"name" => "Ada"})
result["content"]
#=> [%{"type" => "text", "text" => "Hello, Ada!"}]
```

Serve it over stdio from a script or release:

```elixir
:ok = Snodo.Transport.Stdio.serve(Greeter.runtime())
```

or over HTTP, supervised:

```elixir
children = [{Snodo.Transport.StreamableHTTP.Server, runtime: Greeter.runtime(), port: 4000}]
```

The same client connects to either:

```elixir
{:ok, client} = Snodo.Client.connect({:stdio, "elixir", ["greeter.exs"]})
{:ok, client} = Snodo.Client.connect({:http, "http://127.0.0.1:4000/mcp"})
```

## Guides

- [Getting started](https://hexdocs.pm/snodo/getting-started.html)
- [Tools, resources, and prompts](https://hexdocs.pm/snodo/components.html)
- [The client](https://hexdocs.pm/snodo/client.html)
- [Transports](https://hexdocs.pm/snodo/transports.html)
- [Choosing packages for an application](https://hexdocs.pm/snodo/application-stack.html)
- [Interactive operations (MRTR and elicitation)](https://hexdocs.pm/snodo/interactive-operations.html)
- [Subscriptions](https://hexdocs.pm/snodo/subscriptions.html)
- [Authorization](https://hexdocs.pm/snodo/authorization.html)
- [Extensions and Tasks](https://hexdocs.pm/snodo/extensions.html)
- [Instrumentation](https://hexdocs.pm/snodo/instrumentation.html)
- [Initialize-era clients](https://hexdocs.pm/snodo/initialize-era-clients.html)
- [Supported Elixir, OTP, and databases](https://hexdocs.pm/snodo/compatibility.html)
- [Protocol compliance](https://hexdocs.pm/snodo/protocol-compliance.html)

The [examples](https://github.com/joshrotenberg/snodo/blob/main/examples/README.md) are runnable scripts, each checked in CI.

## Protocol support

`2026-07-28` is the default and only required dialect. For clients that still
send `initialize`, enable the older dialects on the server:

```elixir
use Snodo.Server,
  name: "greeter",
  version: "0.1.0",
  protocols: [Snodo.Protocol.V2026_07_28, Snodo.Protocol.V2025_11_25, Snodo.Protocol.V2025_06_18]
```

They cover tools, resources, prompts, completion, and pagination, over HTTP
without sessions and over stdio. They add no session storage.

Against the frozen official conformance suite, all 37 `2026-07-28` server
scenarios pass; that is the pinned runner's score, not a claim of full revision
conformance. On the client side, 31 of 32 pass, including the 25 that cover
OAuth with `snodo_oauth` as the token provider; the other one is excluded from
the score because a 2026-07-28 client sends no `initialize`. Pinned to
`2025-11-25`, the client passes 16 of 18, including all 14 that cover OAuth. The [compliance guide](https://hexdocs.pm/snodo/protocol-compliance.html) lists
what is measured and what is not. Design records from the project's history are
in [docs/history](https://github.com/joshrotenberg/snodo/blob/main/docs/history/README.md).

## Development

```sh
mix setup            # fetch dependencies for every package; rerun after a mix.lock changes
mix quality          # format, compile, Credo, tests, examples, and every sibling package
mix quality.types    # Dialyzer across all nine packages
mix snodo.contract   # the protocol contract inventory
```

Conformance and interop checks against the official TypeScript and Python clients live in
`conformance/` and `interop/`, and run in CI. Releases are made with
release-please; see
[RELEASING.md](https://github.com/joshrotenberg/snodo/blob/main/RELEASING.md).

## Contributing

Open issues are labeled by priority, size, and area, and `good first issue`
marks small, well-scoped starting points.
[CONTRIBUTING.md](https://github.com/joshrotenberg/snodo/blob/main/CONTRIBUTING.md)
describes the workflow.
[AGENTS.md](https://github.com/joshrotenberg/snodo/blob/main/AGENTS.md) lists
the setup, the gate commands CI runs, and the project's constraints, for people
and coding agents alike. Report security problems privately, as described in
[SECURITY.md](https://github.com/joshrotenberg/snodo/blob/main/SECURITY.md).

## License

MIT. See [LICENSE](https://github.com/joshrotenberg/snodo/blob/main/LICENSE).
