Packages
MCP for Elixir and Phoenix: a server for both protocol eras and stdio, with tasks, MCP Apps, middleware and proxying; an OAuth 2.1 authorization server on your own login, or your own; and a client that authorizes itself.
Current section
Files
Jump to
Current section
Files
README.md
# Sake

<p>
<a href="https://hex.pm/packages/sake">
<img alt="Hex Version" src="https://img.shields.io/hexpm/v/sake.svg">
</a>
<a href="https://hexdocs.pm/sake">
<img src="https://img.shields.io/badge/docs-hexdocs-blue" alt="HexDocs">
</a>
<a href="https://github.com/hattoriai/sake/actions">
<img alt="CI Status" src="https://github.com/hattoriai/sake/actions/workflows/ci.yml/badge.svg">
</a>
</p>
**Put your Phoenix app in front of Claude, ChatGPT and every other MCP
client, with your own users signing in.**
Sake (酒) is a Model Context Protocol server, client and OAuth 2.1
authorization server for Elixir. It does three things other MCP
libraries leave to you:
- **Your login is the identity provider.** Sake ships a complete OAuth
2.1 authorization server: discovery, PKCE, dynamic registration,
consent, token rotation. A person connects Claude to your app, signs
in with the account they already have, and every tool call knows who
they are. If you already run Keycloak, Auth0, Okta or Entra ID, Sake
checks their tokens instead.
- **One endpoint serves every client.** The stateless 2026-07-28
protocol and the older handshake-and-session revisions (2025-11-25,
2025-06-18, 2025-03-26) share one path. Old clients and new ones both
connect.
- **The client logs itself in.** `Sake.Client` runs the whole MCP
authorization flow against any server: discovery, registration, PKCE,
refresh, step-up and DPoP. Your code doesn't build URLs or store
tokens.
The official MCP conformance suite runs in CI on every commit (see
[Conformance](#conformance)). MIT licensed.
## Quickstart
```elixir
def deps do
[{:sake, "~> 0.6"}, {:bandit, "~> 1.5"}]
end
```
```elixir
defmodule MyApp.MCP.Echo do
use Sake.Tool, name: "echo", description: "Returns the text it receives"
arg :text, :string, required: true
@impl true
def execute(%{text: text}, _ctx), do: {:ok, text}
end
defmodule MyApp.MCP do
use Sake.Server, name: "my-app", version: "1.0.0"
tools [MyApp.MCP.Echo]
end
```
```elixir
# In your supervision tree:
{Sake, secret_key_base: MyAppWeb.Endpoint.config(:secret_key_base)}
# In your Phoenix router:
import Sake.Router
scope "/" do
sake_endpoint "/mcp", server: MyApp.MCP
end
```
Then connect a client:
```bash
claude mcp add --transport http my-app https://myapp.example.com/mcp
```
Or, on the same machine, over stdio:
```bash
claude mcp add --transport stdio my-app -- mix sake.stdio MyApp.MCP
```
`mix dev` in this repository starts a complete example app with a login
form, the OAuth endpoints, and sample tools, resources and prompts.
## Require a signed-in user
Mark the scope a tool needs. Then turn on `auth:`, add the router
macros, and put your existing login pipeline in front of the authorize
page:
```elixir
defmodule MyApp.MCP.CloseIssue do
use Sake.Tool, name: "close_issue", description: "Close an issue", scope: "issues:write"
arg :id, :integer, required: true
@impl true
def execute(%{id: id}, ctx) do
with {:ok, _issue} <- MyApp.Issues.close(ctx.scope, id), do: {:ok, "Closed ##{id}"}
end
end
```
```elixir
{Sake,
secret_key_base: MyAppWeb.Endpoint.config(:secret_key_base),
auth: [
subject: MyApp.MCP.Subject, # reads the signed-in user from the conn
issuer: "https://myapp.example.com",
servers: [MyApp.MCP],
store: {Sake.Auth.Store.Ecto, repo: MyApp.Repo}
]}
```
```elixir
scope "/" do
sake_well_known()
sake_endpoint "/mcp", server: MyApp.MCP, auth: true
end
scope "/oauth" do
pipe_through :api
sake_oauth_api()
end
scope "/oauth" do
pipe_through [:browser, :require_authenticated_user]
sake_oauth_authorize()
end
```
Now a client that connects gets a 401, finds your authorize URL, and
opens your login page. The person approves on a consent page and the
client gets tokens scoped to this endpoint. Each tool sees the user in
`ctx.scope`. A client without `issues:write` never sees `close_issue`.
Calling it anyway asks the client to step up.
The [quickstart guide](guides/quickstart.md) covers all five steps,
including the subject module and the migration.
## Test it like a controller
`Sake.Test` connects to your server in memory, so a test reads like
real client code. Argument validation, scope checks, errors, telemetry
and traces all run for real. Only HTTP is skipped.
```elixir
import Sake.Test
test "a reader cannot close issues" do
client = connect(MyApp.MCP, scope: user_fixture(), scopes: ["issues:read"])
assert {:ok, tools} = list_tools(client)
refute "close_issue" in Enum.map(tools, & &1.name)
assert {:error, {:insufficient_scope, _}} = call_tool(client, "close_issue", %{"id" => 1})
end
```
## Call other MCP servers
```elixir
{:ok, session} = Sake.Client.connect("https://mcp.example.com/mcp")
{:ok, tools, session} = Sake.Client.list_tools(session)
{:ok, result, session} = Sake.Client.call_tool(session, "search", %{"query" => "x"})
```
When the server asks for authorization, the client runs the flow
itself: discovery, Client ID Metadata Documents or dynamic registration,
PKCE, pushed requests, refresh, and step-up. A service without a person
at a browser can use client credentials, a JWT bearer grant, or
enterprise-managed authorization from an OpenID Connect or SAML sign-on.
See the [client guide](guides/client.md).
## Proxy a third-party server behind your login
Put an upstream MCP server behind your own users, scopes and
middleware, with per-user credentials:
```elixir
defmodule MyApp.MCP.GitHub do
use Sake.Proxy,
name: "github",
version: "1.0.0",
upstream: "https://api.githubcopilot.com/mcp/",
tools: [except: ["delete_repository"]],
scope: "github",
scopes: %{"create_issue" => "github:write"}
@impl Sake.Proxy
def credentials(ctx) do
case MyApp.Accounts.github_token(ctx.scope) do
{:ok, token} -> {:bearer, token}
:error -> {:error, "Connect your GitHub account first."}
end
end
end
```
## What's included
**Protocol**
- Tools, resources, resource templates, prompts and completion
- Elicitation (form and URL), sampling, roots, progress, logging
- Tasks (`io.modelcontextprotocol/tasks`), MCP Apps (`io.modelcontextprotocol/ui`), Skills (`io.modelcontextprotocol/skills`)
- Your own extensions (`Sake.Extension`): methods, settings and notifications under an identifier of yours
- Subscriptions: `subscriptions/listen` on modern clients, `resources/subscribe` on older ones
- Streamable HTTP and stdio, with JSON-RPC batching on 2025-03-26
**Authorization server** (`Sake.Auth`)
- OAuth 2.1 with PKCE, dynamic registration, Client ID Metadata Documents, consent, refresh token rotation
- Bearer or DPoP-bound tokens (RFC 9449), offered or required
- Machine clients: client credentials with a secret, `client_secret_jwt` or `private_key_jwt`
- Enterprise-managed authorization (ID-JAG) and workload identity
- Device authorization grant for CLIs, pushed authorization requests, OpenID Connect (ID tokens, userinfo)
- Step-up on how the person signed in, such as MFA or a recent sign-in (RFC 9470)
- Per-person scope limits and multi-tenant endpoints
**Bring your own authorization server** (`Sake.Auth.External`)
- JWT validation, token introspection, or your own callback, with scopes, step-up and DPoP
**Building and running**
- `Sake.Middleware` around every call: audit, rate limits, plans, rewriting, redaction
- `mount` one server inside another under a prefix, including servers shipped in libraries
- `Sake.Proxy` for an HTTP or stdio upstream on either protocol era
- OpenTelemetry traces from the client's call through the server into the tool
- Several nodes, with shared Ecto stores and `pubsub:` for streams
- `JSON` or `Jason` for your data
The [protocol versions guide](guides/protocol_versions.md) shows which
features each revision supports.
## Conformance
CI runs the official MCP conformance suite
(`@modelcontextprotocol/conformance` 0.2.0-alpha.12) on every commit.
It covers the server and client scenarios of 2025-06-18, 2025-11-25 and
2026-07-28, the authorization-server scenarios, and the extension
scenarios (tasks, skills, and client auth extensions including DPoP).
Every extension and auth scenario passes. Two core scenarios are
expected to fail, and each has a documented reason in
[protocol versions](guides/protocol_versions.md):
| Scenario | Side | Why |
|---|---|---|
| `input-required-result-ignore-extra-params` | server | Answers without the signed request state are asked again |
| `elicitation-sep1034-client-defaults` | client | The client doesn't answer server requests on the standalone GET stream |
## Performance
On a 4-vCPU cloud VM, with a tool that does nothing, so the numbers
measure Sake:
- **About 70 µs of CPU per tool call.** Two cores answer about 8,500
calls a second over HTTP, with a p99 of 2.3 ms at 10 connections.
- **About 20 KiB per open stream.** One broadcast reaches 9,000 open
streams in about a third of a second.
- **Authorization adds 30–60 µs** with the ETS store, cached
introspection or your own callback, and more for signature checks
(DPoP, JWTs) or a database token store.
The scripts are in `bench/`, so you can rerun them on your own hardware.
The [performance guide](guides/performance.md) has every result and the
method behind it.
## Not there yet
- **No Redis store.** State lives in ETS or in your Ecto repo.
- **Pre-1.0.** The API can still change between minor versions. Each
change is in the [changelog](CHANGELOG.md).
## Requirements
Elixir 1.18 or later on OTP 27 or later. CI runs Elixir 1.18 (OTP 27),
1.19 (OTP 28) and 1.20 (OTP 29). Phoenix, Ecto, Phoenix.PubSub, `jose`,
`phoenix_html` and `opentelemetry_api` are optional. Add the ones whose
features you use. Signed JWTs and DPoP need `{:jose, "~> 1.11"}`.
## Guides
- **Start:** [Quickstart](guides/quickstart.md) · [Tools](guides/tools.md) · [Resources and prompts](guides/resources_and_prompts.md) · [Testing](guides/testing.md)
- **Auth:** [Authorization](guides/authorization.md) · [Bring your own authorization server](guides/external_auth.md) · [Multi-tenant endpoints](guides/tenants.md) · [Security](guides/security.md)
- **Protocol:** [Tasks](guides/tasks.md) · [MCP Apps](guides/apps.md) · [Skills](guides/skills.md) · [Extensions](guides/extensions.md) · [Protocol versions](guides/protocol_versions.md) · [Stdio](guides/stdio.md)
- **Architecture:** [Middleware](guides/middleware.md) · [Composing and proxying](guides/composing.md) · [Shipping a server in a library](guides/libraries.md) · [The client](guides/client.md)
- **Operations:** [Observability](guides/observability.md) · [Running on several nodes](guides/clustering.md) · [Performance](guides/performance.md)
## Security
Report vulnerabilities privately, as [SECURITY.md](SECURITY.md)
describes. Please don't open a public issue for one.
## Contributing
[CONTRIBUTING.md](https://github.com/hattoriai/sake/blob/main/CONTRIBUTING.md) explains when a change needs an issue
first and what a pull request needs.
## License
MIT. See [LICENSE](https://github.com/hattoriai/sake/blob/main/LICENSE).