attest-mcp について
attest-mcpは、MCPサーバーのツール呼び出しをスコープ付きアクセスで保護する認証情報検証ミドルウェアです。トークンを検証し、スコープと失効状態を確認して、呼び出しの監査イベントを送信します。
主な機能
- 01ツール呼び出しごとの認証情報検証
- 02署名、有効期限、失効状態の確認
- 03ツール名から必要なスコープを判定
- 04ツール単位または全体のスコープ設定
- 05監査イベントの非同期送信
活用シーン
- 01既存のMCPサーバーへの認証情報検証追加
- 02ツールのスコープに基づく呼び出し制限
- 03認証情報の必須化を段階的に導入
- 04ツール呼び出し記録の監査サービスへの送信
MCP 対応
withAttestでMCPサーバーをラップし、ツールハンドラーの実行前に認証情報とスコープを検証します。失敗時はMCPクライアントに構造化エラーを返し、監査イベントを非同期で送信します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
@attest-dev/sdk/mcp
Model Context Protocol サーバー向けの Attest 認証情報強制ミドルウェアです。
既存の MCP サーバーのすべてのツール呼び出しを、たった 2 行で保護できます。
インストール
npm install @attest-dev/sdk
@attest-dev/sdk には、コアクライアントと MCP ミドルウェアが 1 つのパッケージとして含まれており、2 つのエントリーポイントを備えています。
2 行で統合
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 は server.tool() をその場でパッチし、元の型を維持した同じサーバーオブジェクトを返します。それ以外の connect()、close()、トランスポートは、変更せずに動作します。
仕組み
すべてのツール呼び出しで、ハンドラーの実行前に次の処理を行います。
- 認証情報を抽出します。
extra.authInfo.token(MCP 認証ミドルウェアのパス)、extra.meta.attest_token、またはextra.meta.authorizationから取得します。 - 発行者の JWKS(デフォルトでは 1 回取得し、1 時間キャッシュ)を使って、RS256 署名と有効期限をオフラインで検証します。
scopeForTool()を使ってツール名をスコープに対応付け、認証情報のatt_scopeがそのスコープをカバーしているか確認します。GET /v1/revoked/{jti}を使って失効状態を確認します(プロセス内で 10 秒間キャッシュ)。- 成功した場合、元のハンドラーに処理を引き渡します。
- 失敗した場合は、構造化エラーを返します。エージェントフレームワークには、型付き JSON ペイロードとともに
isError: trueが返されます。 POST /v1/auditに監査イベントを送信します。非同期で実行され、ツール呼び出しをブロックすることはありません。
スコープの対応付け
ツール名は自動的に resource:action 形式のスコープ文字列に対応付けられます。最初の単語(最初の _ より前)がアクションになり、残りがリソースになります。
| ツール名 | 必要なスコープ | 備考 |
|---|---|---|
send_email | email:send | |
read_file | file:read | |
list_documents | documents:list → documents:read | list は read の別名です |
create_user | user:create → user:write | create は write の別名です |
delete_record | record:delete | |
get_weather_forecast | weather_forecast:read | 複数語のリソースではアンダースコアを維持します |
run_query | query:run → query:execute | run は execute の別名です |
アクションの別名 — 次の生のプレフィックスは、正規アクションに対応付けられます。
| 生のプレフィックス | 正規アクション |
|---|---|
create, update, put, patch, set, upsert | write |
get, fetch, list, search, query, find, lookup | read |
remove, destroy | delete |
run, invoke, call | execute |
複数語のリソースでは、スコープ文字列のアンダースコアを維持します。get_weather_forecast → weather_forecast:read となります。これは意図的な仕様で、名詞句全体がリソースとして保持されます。認証情報を発行する際は、同じ文字列を使用してください。scope: ["weather_forecast:read"]。
登録時に上書きするには、server.tool() の最後の引数として AttestToolOptions オブジェクトを渡します。スコープがツール定義の隣に記述されるため、標準的でないツール名のスコープを設定する場合に推奨される方法です。
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"
オプションオブジェクトは withAttest によって使用され、MCP SDK が引数を受け取る前に取り除かれるため、SDK はこのオブジェクトを認識しません。
オプションの toolScopeMap を使ってグローバルに上書きできます(requiredScope を明示的に宣言していないすべてのツールに適用されます)。
const protectedServer = withAttest(server, {
issuerUri: "https://api.attest.dev",
toolScopeMap: {
"send_message": "slack:send",
"query_db": "postgres:read",
},
});
スコープ検出エンドポイント
getAttestScopes() は、稼働中のスコープレジストリを返します。そこには、すべてのツール名と解決済みのスコープ文字列の対応が含まれます。HTTP エンドポイントに接続すれば、認証情報の発行者は、事前の個別調整なしでサーバーが必要とするスコープを照会できます。
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) });
});
レスポンス:
{
"tools": {
"send_email": "email:send",
"gh_create_issue": "github:write"
}
}
オーケストレーターエージェントは、まずこのエンドポイントを取得してから認証情報を発行します。
// 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: "...",
});
スコープ文字列を記したスプレッドシートは不要です。サーバーとクライアント間で手動同期する必要もありません。サーバーが信頼できる唯一の情報源です。
エージェントがブロックされたときに受け取る内容
{
"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 は、これがエラー応答であることを MCP フレームワークに通知します。reason フィールドは機械可読です。
reason | 意味 |
|---|---|
no_credential | リクエストに Attest JWT が見つかりません |
invalid_credential | JWT の署名検証に失敗したか、形式が正しくありません |
credential_expired | JWT の exp が経過しています |
credential_revoked | JTI が失効リストに含まれています |
scope_violation | クレデンシャルに必要なスコープが付与されていません |
オプション
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,
});
},
});
ツールハンドラー内でクレデンシャルのクレームを読み取る
withAttest によるクレデンシャルの検証がすでに完了している場合は、再検証せずにクレームを読み取るために getAttestContext を使用します。
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" }] };
});
段階的なロールアウト
マッピングが正しいと確信できるまではトラフィックをブロックせずに、既存のサーバーに Attest を追加します。
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,
}),
});
監視で違反率がゼロになったら、requireCredential を true に切り替えます。
監査ログ
ツールの呼び出しはすべて、成功・失敗を問わず、Attest サーバーに POST /v1/audit を送信します。配信は非同期で、ツールの実行をブロックすることはありません。
監査ログの配信はベストエフォートであり、エラーは構造化された形で通知されます。確実に配信するには、onAuditError を設定し、独自の再試行キューを実装してください。
失敗は優先順位に従って通知され、暗黙のうちに破棄されることはありません。
onAuditError(error, event)— 再試行キュー、フォールバックログ、ページャーなどで処理しますonViolation({ reason: "audit_failure", ... })—onAuditErrorが設定されていない場合、既存の監視に通知しますconsole.warn— どちらのコールバックも設定されていない場合の最終手段です
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 }),
});
監査エンドポイントからの 2xx 以外の応答は、配信成功ではなく配信失敗として扱われます。
監査イベントには、event_type、jti、att_tid、att_uid、agent_id(mcp:<toolName>)、scope、および outcome メタフィールド(allowed、scope_violation、blocked_revoked)が含まれます。
タスクツリーの監査チェーンを照会します。
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));
完全な例
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);
ライセンス
Apache-2.0
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。