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 分