A server whose tools, resources and prompts are another MCP server's:
every call is forwarded to that upstream with Sake.Client, and the
proxy adds what the upstream lacks, your login and scopes, middleware
(access control, audit, rate limits), on one approved endpoint.
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
endA proxy is a Sake.Server: serve it on its own endpoint, or mount
it in another server, under a prefix, next to local modules and other
proxies (see Sake.Server.mount/2). It may also declare tools of its
own and middleware, which runs around the proxied components as around
any other. Add it to the supervision tree, after Sake; it keeps the
upstream sessions:
children = [
{Sake, secret_key_base: ...},
{MyApp.MCP.GitHub, upstream: System.fetch_env!("GITHUB_MCP_URL")}
]Options
The Sake.Server options (name:, version: and the others), and:
:upstream(required) — the URL of a Streamable HTTP server, or{:stdio, command, args}for a server the proxy starts as a subprocess.:connect— options forSake.Client.connect/2:era:,headers:(a fixed token:[{"authorization", "Bearer ..."}]),auth:(the client's own authorization, such as a machine client'sclient_credentials),req:, and for stdioenv:,cd:andstderr:.:toolsand:prompts—true(the default),false, or[only: names]/[except: names], by the upstream's names.:resources—true(the default) orfalse.:scope— one or more scopes every proxied component requires.:scopes— a map from an upstream tool or prompt name to the scopes it requires on top of:scope.:list_ttl— how long, in ms, an upstream's lists are kept when it does not say (a legacy upstream); default 60 000. A modern upstream's ownttlMsis used when it gives one.:idle_timeout— how long, in ms, an unused upstream session is kept; default 600 000.
Each option may also be given at runtime, in the child spec, where it
overrides the one given to use.
Credentials
Without credentials/1, the proxy calls the upstream with what
:connect gives it: no credentials, a fixed token in headers:, or
Sake.Client's own authorization (auth:). With it, each request's
principal acts as itself upstream: the callback gets the request's
Sake.Context and returns {:bearer, token}, {:headers, headers},
:none, or {:error, message} to refuse (the lists leave the proxied
components out, and a call gets message).
Upstream sessions are kept per principal (the token's subject, else
the host's scope) and per credential: one principal's session, token,
tasks or requests are never used for another. A stdio upstream is one
process for everyone (its credentials come from its environment, and
credentials/1 does not apply), and the proxy sends it one request at
a time, since a stdio server cannot
tell its callers' questions and log messages apart. A session the
upstream refuses (a 401, an expired legacy session) is dropped and
opened again once, with fresh credentials.
What is forwarded
See "Proxying another server" in guides/composing.md for the
details: the lists (cached by the upstream's ttlMs), calls, reads,
prompts and completions; progress and log messages; elicitation,
sampling and roots requests, across eras; an upstream's tasks, which
become the proxy's own; and, when every principal has the same
upstream identity, the upstream's list changes and the updates of the
resources clients subscribed to, broadcast as the proxy's.
Summary
Types
What credentials/1 returns.
Callbacks
The credentials the request's principal calls the upstream with. See "Credentials" above.
Types
Callbacks
@callback credentials(ctx :: Sake.Context.t()) :: credentials()
The credentials the request's principal calls the upstream with. See "Credentials" above.