Auth for your MCP server, without running an authorization server

Auth for your MCP server, without running an authorization server

为你的 MCP 服务器实现身份验证,无需运行授权服务器

You built a remote MCP server. It’s useful, so now strangers’ agents want to call it, and you need to answer the question every remote server hits: who’s allowed in? The MCP spec’s answer is OAuth 2.1: put an authorization server in front, register clients, issue tokens. Which is correct, and heavy. An authorization server is a stateful, security-critical service with a user database — you run it or you rent it, and either way it’s now load-bearing infrastructure for what might be a weekend project. It also assumes your callers can do an OAuth dance, and that you want accounts.

你构建了一个远程 MCP 服务器。它很有用,所以现在陌生人的智能体(Agents)想要调用它,而你需要回答每个远程服务器都会遇到的问题:谁被允许访问?MCP 规范给出的答案是 OAuth 2.1:在前端放置一个授权服务器,注册客户端,并颁发令牌。这虽然正确,但很沉重。授权服务器是一个有状态的、对安全性至关重要的服务,且带有用户数据库——无论你是自己运行还是租用,它现在都成了你原本可能只是个周末项目的“承重基础设施”。此外,它还假设调用者能够完成 OAuth 的交互流程,并且你确实需要管理用户账户。

Here’s the shape with no authorization server anywhere: the credential — called a deed — certifies itself. An agent mints one locally by signing a challenge you issued (or by proving fleet membership in zero knowledge — more below). Your server verifies it with a library: local crypto plus one eth_call to a public registry contract on Base, served by any RPC provider. No token service, no client registration, no user table.

以下是完全不需要授权服务器的方案:凭证(称为“契约”/deed)可以自我证明。智能体通过签署你发出的挑战(或通过零知识证明其属于某个集群——详见下文)在本地生成凭证。你的服务器使用一个库来验证它:本地加密计算加上一次对 Base 链上公共注册合约的 eth_call(由任何 RPC 提供商提供服务)。无需令牌服务,无需客户端注册,也无需用户表。

Your whole auth stack is three routes: 你的整个身份验证栈只需三个路由:

import { DeedVerifier, sessionJwt } from "@grantor/verify";
import { grantorExpress } from "@grantor/verify/express";
import { Registry } from "@grantor/verify/registry";

const verifier = new DeedVerifier(
  RPC_URL, Registry.canonical(), CHAIN_ID, TENANT_ID, 
  AUDIENCE, ORIGIN, MAX_TTL_SECS, CACHE_TTL_SECS, 
  false, Math.floor(Date.now() / 1000),
);

const g = grantorExpress({
  verifier, app,
  challengeEndpoint: "/auth/challenge",
  chainId: CHAIN_ID,
  modes: ["user-sig", "agent-zk"],
  vouchSignature: VOUCH_SIGNATURE,
  vouchEpoch: VOUCH_EPOCH,
  vouchExp: VOUCH_EXP,
});

app.get("/auth/challenge", g.challenge);

That one call also auto-publishes GET /.well-known/grantor-deed — a discovery document naming your tenant, chain, modes, and challenge endpoint — and self-checks it at startup, so a misconfiguration fails your boot, not your first user’s login. The second route exchanges a deed for a session, the way a token endpoint would — an MCP client authenticates once, not per request:

这一调用还会自动发布 GET /.well-known/grantor-deed ——这是一个发现文档,列出了你的租户、链、模式和挑战端点——并在启动时进行自检,因此配置错误会导致启动失败,而不是等到第一个用户登录时才报错。第二个路由用于将“契约”交换为会话,就像令牌端点所做的那样——MCP 客户端只需验证一次,而不是每次请求都验证:

app.post("/auth/token", async (req, res) => {
  const { deed, challenge } = req.body;
  const claims = await g.guard.verify(JSON.stringify(deed), challenge);
  // claims.sub is a verified, pseudonymous subject — recomputed by the
  // verifier, not read from the deed. Mint YOUR session from it:
  res.json({ session_jwt: sessionJwt(claims.sub, AUDIENCE, BigInt(TENANT_ID), SIGNING_KEY_PEM, BigInt(now), BigInt(TTL), { iss: ORIGIN }) });
});

sessionJwt mints a plain ES256 JWT with your key — any JOSE library verifies it without ever importing this SDK. The third route is your existing MCP transport, gated on that session. That’s the entire surface.

sessionJwt 使用你的密钥生成一个普通的 ES256 JWT——任何 JOSE 库都可以验证它,而无需导入此 SDK。第三个路由是你现有的 MCP 传输层,由该会话进行门控。这就是全部的实现面。

Three properties you don’t usually get from a weekend auth setup: 你通常无法从周末开发的身份验证方案中获得以下三个特性:

  1. Rejections teach the caller. Every 401 carries WWW-Authenticate: Grantor-Deed … plus discovery/learn fields pointing at your discovery document and a machine-readable onboarding manifest. The guard also serves RFC 9728 protected-resource metadata, so an MCP-spec OAuth client discovers what your server needs the standard way. A capable agent that gets rejected can read its way to authenticated — no human writes an integration ticket. (All of it opt-out with one flag if you want silent 401s.)

  2. 拒绝信息会引导调用者。 每个 401 错误都携带 WWW-Authenticate: Grantor-Deed……以及指向你的发现文档和机器可读的接入清单的发现/学习字段。该防护机制还提供 RFC 9728 受保护资源元数据,因此符合 MCP 规范的 OAuth 客户端可以以标准方式发现你的服务器需求。一个有能力的智能体在被拒绝后,可以通过读取这些信息完成身份验证——无需人工编写集成工单。(如果你想要静默的 401 错误,所有这些都可以通过一个标志关闭。)

  3. You can authorize a whole fleet without an allowlist. With agent-zk in modes, any agent enrolled in your tenant’s on-chain registry proves membership in zero knowledge. Membership is the authorization — no per-agent config on your server, and you learn a stable pseudonym per agent, not a wallet address.

  4. 无需白名单即可授权整个集群。 在模式中启用 agent-zk 后,任何在你的租户链上注册表中登记的智能体都可以通过零知识证明其成员身份。成员身份即授权——你的服务器上无需针对每个智能体进行配置,并且你获得的是每个智能体稳定的化名,而不是钱包地址。

  5. Billing is enforced where verification happens. The verifier checks the tenant’s on-chain status during the same read, and fails closed if the chain is unreachable. Nobody can verify deeds against a lapsed tenant.

  6. 计费在验证发生时强制执行。 验证器在读取过程中会同时检查租户的链上状态,如果链不可达,则会拒绝访问。没有人能针对已过期的租户验证“契约”。

What it costs, honestly: you need a tenant on the registry (createTenant plus USDC funding on Base — a few contract calls, no signup form, because there is no server to sign up with) and a signed origin vouch for wherever your server runs, which is what lets a well-behaved agent refuse to authenticate to a hostile origin impersonating you. 老实说,它的成本是:你需要在注册表上创建一个租户(调用 createTenant 并通过 Base 链上的 USDC 充值——只需几次合约调用,没有注册表单,因为根本没有服务器供你注册),并为你服务器运行的任何地方提供一个签名的来源证明(origin vouch),这能让表现良好的智能体拒绝向冒充你的恶意来源进行身份验证。

The whole thing is an unaudited developer preview. The guard ships in TypeScript, Python, Go, and Rust, so this isn’t an Express-only story. Full guide, transcribed from a runnable reference server: https://chaingrantor.com/docs/guide/mcp-server — and #1 in this series covers the other direction, gating what your own sub-agents may do (including wrapping any third-party MCP server so enforcement is structural, not voluntary): https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h

整个项目目前是一个未经审计的开发者预览版。该防护库支持 TypeScript、Python、Go 和 Rust,因此这不仅仅是 Express 的专属方案。完整指南摘自一个可运行的参考服务器:https://chaingrantor.com/docs/guide/mcp-server —— 该系列的第 1 篇涵盖了另一个方向,即限制你自己的子智能体可以做什么(包括封装任何第三方 MCP 服务器,使强制执行成为结构性的,而非自愿的):https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h

OAuth told us auth needs an authorization server. For agents, it needs an authorization — the server part turns out to be optional. OAuth 告诉我们身份验证需要一个授权服务器。对于智能体而言,它只需要一个“授权”——事实证明,服务器部分是可选的。