Attest Mcp

io.github.chudah1由 GitHub 账号 chudah1 发布v0.1.0更新于 Oct 24, 2026

Credential enforcement middleware for MCP servers — verifies scoped tokens on every tool call

已验证STDIO仅桌面开发者工具安全与监控

概览

AI 生成的概览

为现有 MCP 服务器加装的中间件,让每次工具调用都需通过带作用域的 Attest 凭证校验,并记录审计事件。

可以做什么
  • 包装现有 MCP 服务器,使每次工具调用在执行前都校验 Attest 凭证
  • 离线校验凭证签名与有效期(基于签发方 JWKS),并检查是否被吊销
  • 自动把工具名映射为 resource:action 作用域,支持单工具或全局覆盖
  • 调用被拦截时向调用方代理返回结构化的机器可读错误
  • 对外暴露解析后的作用域清单,并可在处理函数内读取已校验的凭证声明
  • 为每次工具调用发送审计事件,并提供违规与投递失败回调
适用场景
适合已经运行自有 MCP 服务器、希望在不重写工具处理函数的前提下实现按工具授权、委派追踪与审计留痕的开发者。也适合先观察记录违规、确认无误后再开启拦截的渐进式上线。
运行要求
以本地 npm 包形式加入你自己的 MCP 服务器项目,需与 MCP SDK 配合使用。需要一个可访问的 Attest 签发服务器(清单中通过 ATTEST_BASE_URL 配置),需要网络访问以查询 JWKS 与吊销状态,并需要该服务器签发的凭证。
安装前请注意
它是给服务器开发者用的库,不是独立可用的工具服务器,单独安装不会提供任何工具。每次工具调用都会向配置的 Attest 服务器发送审计事件并查询吊销状态,工具名与凭证标识会离开本机。审计投递默认尽力而为,除非自行实现重试。将 requireCredential 设为 false 只会记录违规而不拦截。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Attest Mcp,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

@attest-dev/sdk/mcp

Attest credential enforcement middleware for Model Context Protocol servers.

Two lines to protect every tool call on an existing MCP server.

Install

bash
npm install @attest-dev/sdk

@attest-dev/sdk ships both the core client and the MCP middleware as a single package with two entry points.

Two-line integration

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { withAttest } from "@attest-dev/sdk/mcp";
const server = new McpServer({ name: "my-tools", version: "1.0.0" });const protectedServer = withAttest(server, {  issuerUri: "https://api.attest.dev",});
// Register tools exactly as before — every call is now credential-gated.protectedServer.tool("send_email", schema, handler);

withAttest patches server.tool() in place and returns the same server object typed as the original. Everything else — connect(), close(), transports — works unchanged.

How it works

On every tool call, before the handler executes:

  1. Extract credential from extra.authInfo.token (MCP auth middleware path), extra.meta.attest_token, or extra.meta.authorization
  2. Verify RS256 signature + expiry offline against the issuer's JWKS (fetched once, cached for 1 hour by default)
  3. Map tool name → scope using scopeForTool() and check the credential's att_scope covers it
  4. Check revocation via GET /v1/revoked/{jti} (10-second in-process cache)
  5. Pass through to the original handler on success
  6. Return a structured error on any failure — agent frameworks receive isError: true with a typed JSON payload
  7. Fire audit event to POST /v1/audit — async, never blocks the tool call

Scope mapping

Tool names are automatically mapped to resource:action scope strings. The first word (before the first _) becomes the action; the remainder becomes the resource.

Tool nameRequired scopeNotes
send_emailemail:send
read_filefile:read
list_documentsdocuments:list → documents:readlist aliases to read
create_useruser:create → user:writecreate aliases to write
delete_recordrecord:delete
get_weather_forecastweather_forecast:readmulti-word resources keep underscores
run_queryquery:run → query:executerun aliases to execute

Action aliases — these raw prefixes map to canonical actions:

Raw prefixCanonical action
create, update, put, patch, set, upsertwrite
get, fetch, list, search, query, find, lookupread
remove, destroydelete
run, invoke, callexecute

Multi-word resources keep their underscores in the scope string. get_weather_forecast → weather_forecast:read. This is intentional — the full noun phrase is preserved as the resource. When issuing credentials, use the same string: scope: ["weather_forecast:read"].

Override at registration — pass a AttestToolOptions object as the final argument to server.tool(). This is the preferred way to set scope for non-standard tool names because the scope lives next to the tool definition:

typescript
import { withAttest } from "@attest-dev/sdk/mcp";import type { AttestToolOptions } from "@attest-dev/sdk/mcp";
protectedServer.tool("gh_create_issue", schema, handler, {  requiredScope: "github:write",} satisfies AttestToolOptions);
protectedServer.tool("stripe_charge", schema, handler, {  requiredScope: "stripe:write",} satisfies AttestToolOptions);
// Auto-mapping still applies when requiredScope is omitted.protectedServer.tool("send_email", schema, handler);// → "email:send"

The options object is consumed by withAttest and stripped before the MCP SDK sees the arguments — the SDK is unaware of it.

Override globally using toolScopeMap in options (applies to all tools that don't declare requiredScope explicitly):

typescript
const protectedServer = withAttest(server, {  issuerUri: "https://api.attest.dev",  toolScopeMap: {    "send_message": "slack:send",    "query_db":     "postgres:read",  },});

Scope discovery endpoint

getAttestScopes() returns the live scope registry — every tool name mapped to its resolved scope string. Wire it to an HTTP endpoint so credential issuers can query what scopes a server requires without out-of-band coordination.

typescript
import { withAttest, getAttestScopes } from "@attest-dev/sdk/mcp";
const protectedServer = withAttest(server, { issuerUri: "https://api.attest.dev" });
protectedServer.tool("send_email", schema, handler);protectedServer.tool("gh_create_issue", schema, handler, { requiredScope: "github:write" });
// Express / Hono / any HTTP framework:app.get("/.well-known/attest-scopes", (_req, res) => {  res.json({ tools: getAttestScopes(protectedServer) });});

Response:

json
{  "tools": {    "send_email":      "email:send",    "gh_create_issue": "github:write"  }}

An orchestrator agent issues credentials by fetching this endpoint first:

typescript
// Before starting a task, discover what the MCP server needs:const { tools } = await fetch("https://my-mcp-server/.well-known/attest-scopes")  .then(r => r.json());
// Issue a credential with exactly those scopes:const { token } = await attestClient.issue({  agent_id: "my-agent",  user_id:  "usr_alice",  scope:    Object.values(tools),   // ["email:send", "github:write"]  instruction: "...",});

No spreadsheet of scope strings. No manual sync between server and client. The server is the source of truth.

What an agent receives when blocked

json
{  "content": [    {      "type": "text",      "text": "{\n  \"error\": \"attest_violation\",\n  \"reason\": \"scope_violation\",\n  \"detail\": \"Credential (jti: abc-123) grants [research:read] but tool \\\"send_email\\\" requires \\\"email:send\\\".\",\n  \"jti\": \"abc-123\",\n  \"taskId\": \"tid-xyz\"\n}"    }  ],  "isError": true}

isError: true signals the MCP framework that this is an error response. The reason field is machine-readable:

reasonMeaning
no_credentialNo Attest JWT found in the request
invalid_credentialJWT signature verification failed or malformed
credential_expiredJWT exp has passed
credential_revokedJTI is in the revocation list
scope_violationCredential does not grant the required scope

Options

typescript
withAttest(server, {  // Required: URI of your Attest server  issuerUri: "https://api.attest.dev",
  // How long to cache JWKS before re-fetching (seconds). Default: 3600  jwksCacheTTL: 3600,
  // How long to cache revocation status per JTI (seconds).  // Set 0 to disable caching (every call hits the server). Default: 10  revocationCacheTTL: 10,
  // false = log violations via onViolation but don't block the call.  // Useful for gradual rollout. Default: true  requireCredential: true,
  // Per-tool scope overrides. Keys are exact tool names.  toolScopeMap: {    "send_message": "slack:send",  },
  // Called on every scope violation (synchronous).  // Use for metrics, alerting, structured logging.  onViolation: (event) => {    console.warn("Attest violation", {      tool:     event.toolName,      reason:   event.reason,      required: event.requiredScope,      granted:  event.grantedScope,      jti:      event.jti,    });  },});

Reading credential claims inside a tool handler

After withAttest has already verified the credential, use getAttestContext to read the claims without re-verifying:

typescript
import { withAttest, getAttestContext } from "@attest-dev/sdk/mcp";
protectedServer.tool("send_email", schema, async (args, extra) => {  const ctx = getAttestContext(extra);  // ctx.att_uid   — the human who initiated the task  // ctx.att_tid   — task tree ID (use for audit correlation)  // ctx.att_scope — granted scopes  // ctx.att_depth — delegation depth (0 = root)  // ctx.jti       — this credential's unique ID
  await sendEmail(args.to, args.body, { actingAs: ctx?.att_uid });  return { content: [{ type: "text", text: "sent" }] };});

Gradual rollout

Add Attest to an existing server without blocking traffic until you're confident the mapping is correct:

typescript
const protectedServer = withAttest(server, {  issuerUri: "https://api.attest.dev",  requireCredential: false,   // observe, don't block  onViolation: (event) => metrics.increment("attest.violation", {    tool:   event.toolName,    reason: event.reason,  }),});

Flip requireCredential to true once violation rates drop to zero in your monitoring.

Audit logging

Every tool call — pass or fail — fires a POST /v1/audit to your Attest server. Delivery is async and never blocks tool execution.

Audit delivery is best-effort with structured error surfacing. For guaranteed delivery, configure onAuditError and implement your own retry queue.

Failures surface in priority order — nothing is ever silently dropped:

  1. onAuditError(error, event) — you handle it (retry queue, fallback log, pager)
  2. onViolation({ reason: "audit_failure", ... }) — surfaces to your existing monitoring if onAuditError is not set
  3. console.warn — last resort if neither callback is configured
typescript
const protectedServer = withAttest(server, {  issuerUri: "https://api.attest.dev",
  // Tier 1: implement a retry queue for guaranteed delivery  onAuditError: (err, event) => {    retryQueue.push({ event, attempts: 0, nextRetry: Date.now() + 5000 });    logger.error("attest audit delivery failed", {      jti:      event.jti,      agent_id: event.agent_id,      error:    err.message,    });  },
  // Tier 2: violations (including audit_failure) go to your monitoring  onViolation: (e) => metrics.increment("attest.event", { reason: e.reason }),});

Non-2xx responses from the audit endpoint are treated as delivery failures, not silent successes.

The audit event carries: event_type, jti, att_tid, att_uid, agent_id (mcp:<toolName>), scope, and an outcome meta field (allowed, scope_violation, blocked_revoked).

Query the audit chain for a task tree:

typescript
import { AttestClient } from "@attest-dev/sdk";
const client = new AttestClient({ baseUrl: "https://api.attest.dev", apiKey: "..." });const chain = await client.audit(taskId);chain.events.forEach(e => console.log(e.event_type, e.jti, e.created_at));

Full example

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { withAttest } from "@attest-dev/sdk/mcp";import { z } from "zod";
const server = new McpServer({ name: "email-server", version: "1.0.0" });
server.tool(  "send_email",  { to: z.string().email(), subject: z.string(), body: z.string() },  async ({ to, subject, body }) => {    await sendEmail(to, subject, body);    return { content: [{ type: "text", text: `Sent to ${to}` }] };  },);
// Wrap — every tool now requires a valid credential with "email:send" scope.const protectedServer = withAttest(server, {  issuerUri: process.env.ATTEST_URI ?? "http://localhost:8080",  onViolation: (e) => console.warn("blocked:", e.reason, e.toolName),});
const transport = new StdioServerTransport();await protectedServer.connect(transport);

License

Apache-2.0

来源:sdks/typescript/mcp/README.md,提交 defffb9

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

1
  1. v0.1.0最新Sep 16, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分