Sake

Sake — MCP for Phoenix

Hex Version HexDocs CI Status

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:

The official MCP conformance suite runs in CI on every commit (see Conformance). MIT licensed.

Quickstart

def deps do
  [{:sake, "~> 0.6"}, {:bandit, "~> 1.5"}]
end
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
# 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:

claude mcp add --transport http my-app https://myapp.example.com/mcp

Or, on the same machine, over stdio:

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:

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
{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}
 ]}
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 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.

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

{: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.

Proxy a third-party server behind your login

Put an upstream MCP server behind your own users, scopes and middleware, with per-user credentials:

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

Authorization server (Sake.Auth)

Bring your own authorization server (Sake.Auth.External)

Building and running

The protocol versions guide 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:

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:

The scripts are in bench/, so you can rerun them on your own hardware. The performance guide has every result and the method behind it.

Not there yet

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

Security

Report vulnerabilities privately, as SECURITY.md describes. Please don't open a public issue for one.

Contributing

CONTRIBUTING.md explains when a change needs an issue first and what a pull request needs.

License

MIT. See LICENSE.