普通视图

发现新文章,点击刷新页面。
昨天以前首页

MCP OAuth 2.0 认证

作者 wusfe
2026年4月13日 12:28

MCP 鉴权机制详解:基于 OAuth 2.0 的标准实践

前言

MCP(Model Context Protocol)作为连接 AI 助手与外部工具的桥梁,其安全性至关重要。本项目(demo)演示了一套完整的 OAuth 2.0 授权码流程实现,采用标准 Localhost Callback 方案,让 AI 客户端(如 Claude Code)能够安全地访问受保护的 MCP 工具。


1. MCP 鉴权概述

MCP 协议本身支持多种认证机制,其中最标准的方式是借助 OAuth 2.0 授权框架。MCP 鉴权的核心目标是:

  • 身份验证:确认客户端的身份(who are you)
  • 授权控制:决定客户端可以访问哪些工具(what you can do)
  • 会话管理:维护多次请求之间的状态(session)

2. OAuth 2.0 核心概念

2.1 角色定义

角色 说明
Resource Owner 资源所有者,即最终用户
Client 想要访问资源的应用程序(此处为 Claude Code)
Authorization Server 颁发访问令牌的授权服务器
Resource Server 托管受保护资源的服务器(MCP 端点)

2.2 授权码流程(Authorization Code Flow)

┌─────────┐     ┌─────────┐     ┌─────────────┐     ┌───────────┐
│  Client │     │ Browser │     │ Auth Server │     │Token Server│
└────┬────┘     └────┬────┘     └──────┬──────┘     └─────┬─────┘
     │               │                 │                  │
     │ ① 打开授权页  │                 │                  │
     │──────────────▶│                 │                  │
     │               │ ② 显示授权页面  │                  │
     │               │◀────────────────│                  │
     │               │ ③ 用户点击授权  │                  │
     │               │────────────────▶│                  │
     │               │                 │ ④ 生成 code      │
     │               │◀────────────────│  重定向         │
     │ ⑤ code       │                 │                  │
     │◀─────────────│                 │                  │
     │               │                 │                  │
     │ ⑥ 用 code 换 token             │                  │
     │────────────────────────────────▶│                  │
     │               │                 │ ⑦ 返回 token    │
     │◀────────────────────────────────│                  │
     │               │                 │                  │
     │ ⑧ 带 token 访问 MCP             │                  │
     │────────────────────────────────────────────────────▶│

3. 标准 Localhost Callback 方案

本项目采用标准 Localhost Callback 方案,这是 OAuth 2.0 中最安全的公共客户端实现之一。

3.1 方案原理

┌────────────────────────────────────────────────────────────────────┐
│                        Localhost Callback 流程                      │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Client (localhost:3000)                                           │
│        │                                                            │
│        │ ① 启动本地 HTTP 服务器                                      │
│        │                                                            │
│        │ ② 打开浏览器到 Auth Server                                 │
│        ▼                                                            │
│   ┌─────────────┐         Auth Server (localhost:3005)             │
│   │  Browser    │                                                │
│   └──────┬──────┘                                                │
│          │                                                         │
│          │ ③ 用户授权后重定向到                                    │
│          │    localhost:3000/callback?code=xxx                    │
│          ▼                                                         │
│   ┌─────────────┐                                                 │
│   │ Local Server│ ← 同一台机器,同一进程                            │
│   │ 收到 code   │   安全地接收到授权码                              │
│   └─────────────┘                                                 │
│          │                                                         │
│          │ ④ code 通过内存传递(无需网络)                          │
│          ▼                                                         │
│   Client 继续:                                                    │
│          │                                                         │
│          │ ⑤ 用 code 向 /token 换取 access_token                   │
│          │                                                         │
│          │ ⑥ 用 access_token 调用 MCP 端点                          │
│          ▼                                                         │
│   MCP Resource Server                                              │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘

3.2 为什么选择 Localhost Callback?

方案 优点 缺点
Localhost Callback 无额外基础设施,code 在本机传递,安全 仅限桌面客户端
Private URI Scheme 可自定义回调协议 需要系统配置,可能被拦截
Loopback Interface 类似 localhost,跨平台 部分平台可能受限

4. 项目架构

4.1 整体架构图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Claude Code (MCP Client)                        │
│                                                                             │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐                     │
│   │ OAuth SDK   │    │ MCP Client  │    │  Transport  │                     │
│   │ - register  │    │ - listTools │    │ - HTTP/SSE  │                     │
│   │ - authorize │    │ - callTool  │    │ - Session   │                     │
│   └─────────────┘    └─────────────┘    └─────────────┘                     │
└─────────────────────────────────────────────────────────────────────────────┘
                              │
                              │  OAuth + MCP
                              ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         demo Server (Express)                                │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                         OAuth Provider                               │ │
│   │   ┌───────────────────┐  ┌───────────────────┐  ┌───────────────┐    │ │
│   │   │ InMemoryClients  │  │   Auth Codes      │  │    Tokens     │    │ │
│   │   │     Store        │  │     Map           │  │     Map       │    │ │
│   │   └───────────────────┘  └───────────────────┘  └───────────────┘    │ │
│   │                                                                          │ │
│   │   端点: /register, /authorize, /token, /.well-known/oauth-*            │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                    StreamableHTTPServerTransport                      │ │
│   │   • Session 管理  • Bearer Token 验证  • SSE 流式响应                 │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                         MCP Server (McpServer)                       │ │
│   │   工具: public-info (公共), protected-data (需认证)                   │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

4.2 目录结构

demo/
├── src/
│   ├── server.ts      # OAuth 授权服务器 + MCP 服务器
│   └── client.ts      # OAuth 客户端(MCP 消费者)
├── build/             # 编译输出
├── package.json
├── tsconfig.json
└── .env.example       # 环境变量示例

5. 服务器端实现

5.1 核心组件:DemoOAuthServerProvider

class DemoOAuthServerProvider {
  clientsStore = new InMemoryClientsStore();

  // 授权码存储(10分钟过期)
  private codes = new Map<string, {
    client: OAuthClientInformationFull;
    expiresAt: number;
  }>();

  // 访问令牌存储(1小时过期)
  private tokens = new Map<string, {
    clientId: string;
    scopes: string[];
    expiresAt: number;
  }>();

  // 刷新令牌存储
  private refreshTokens = new Map<string, {
    clientId: string;
    scopes: string[];
  }>();
}

5.2 授权端点(/authorize)

用户访问授权页面,确认后生成授权码并重定向:

app.get('/authorize', (req, res) => {
  const { client_id, redirect_uri, state, scope } = req.query;
  
  // 返回授权确认页面
  res.send(`
    <h1>Authorization Request</h1>
    <form method="POST" action="/authorize/approve">
      <button type="submit">Authorize</button>
    </form>
  `);
});

// 处理授权批准
app.post('/authorize/approve', async (req, res) => {
  await oauthProvider.authorize(client, {
    redirectUri: redirect_uri,
    state
  }, res);  // 重定向到 localhost:3000/callback?code=xxx
});

5.3 Token 端点(/token)

接收授权码,返回访问令牌:

app.post('/token', async (req, res) => {
  const { grant_type, code, client_id, client_secret } = req.body;

  if (grant_type === 'authorization_code') {
    const tokens = await oauthProvider.exchangeCodeForToken(code);
    res.json(tokens);  // { access_token, token_type, expires_in, ... }
  }
});

5.4 MCP 端点(/mcp)—— Bearer 认证

使用 requireBearerAuth 中间件保护 MCP 端点:

app.use('/mcp', 
  requireBearerAuth({
    verifier: oauthProvider,
    requiredScopes: ['mcp:tools']
  }), 
  mcpRouter
);

5.5 工具注册

// 公共工具 - 无需认证
server.registerTool('public-info', {
  description: 'Get public server information (no auth required)'
}, async () => ({ content: [{ type: 'text', text: 'Public info' }] }));

// 受保护工具 - 需要认证
server.registerTool('protected-data', {
  description: 'Get sensitive data (requires Bearer authentication)'
}, async () => ({ content: [{ type: 'text', text: 'Protected data' }] }));

6. 客户端实现

6.1 启动本地回调服务器

async function startCallbackServer(): Promise<{ code: string; server: http.Server }> {
  return new Promise((resolve, reject) => {
    const server = http.createServer((req, res) => {
      const url = new URL(req.url || '/', `http://localhost:${LOCAL_PORT}`);
      
      if (url.pathname === '/callback') {
        const code = url.searchParams.get('code');
        
        if (code) {
          res.end('<h1>Authorization Successful!</h1>');
          resolve({ code, server });
        }
      }
    });
    
    server.listen(LOCAL_PORT);
  });
}

6.2 构建授权 URL

const authUrl = new URL(`${AUTH_SERVER_URL}/authorize`);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', LOCAL_CALLBACK_URL);  // http://localhost:3000/callback
authUrl.searchParams.set('scope', 'mcp:tools');
authUrl.searchParams.set('response_type', 'code');

await open(authUrl.toString());  // 打开浏览器

6.3 交换 Token

async function getAccessToken(authCode: string): Promise<string> {
  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: LOCAL_CALLBACK_URL,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET
  });

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString()
  });

  const tokens = await response.json();
  return tokens.access_token;
}

6.4 创建带认证的 Transport

const transport = new StreamableHTTPClientTransport(SERVER_URL, {
  requestInit: {
    headers: {
      'Authorization': `Bearer ${accessToken}`  // Bearer Token 认证
    }
  }
});

const client = new Client({ name: 'demo-client', version: '1.0.0' }, {});
await client.connect(transport);

// 调用工具
const tools = await client.listTools();
const result = await client.callTool({ name: 'protected-data', arguments: {} });

7. 完整数据流时序图

┌───────────┐     ┌───────────┐     ┌───────────────────────────────────┐
│  Client   │     │  Browser  │     │          Auth Server              │
└─────┬─────┘     └─────┬─────┘     └───────────────┬───────────────────┘
      │                 │                           │
      │ ① 启动本地服务器                             │
      │     localhost:3000                         │
      │                                           │
      │ ② 打开授权页面                              │
      │───────────────────────────────────────────▶ GET /authorize
      │                 │                         │
      │                 │ ③ 显示授权页面           │
      │                 │◀─────────────────────────│
      │                 │                         │
      │                 │ ④ 用户点击 Authorize    │
      │                 │─────────────────────────▶│ POST /authorize/approve
      │                 │                         │
      │                 │ ⑤ 重定向到              │
      │                 │   localhost:3000/callback?code=xxx
      │◀───────────────────────────────────────────│
      │                 │                         │
      │ ⑥ 收到 code    │                         │
      │    (本地进程)    │                         │
      │                 │                         │
      │ ⑦ 用 code 换 token                        │
      │───────────────────────────────────────────▶│ POST /token
      │                 │                         │
      │ ⑧ 收到 access_token                       │
      │◀───────────────────────────────────────────│
      │                 │                         │
      │ ⑨ 带 Bearer Token 调用 MCP               │
      │───────────────────────────────────────────▶│ POST /mcp
      │                 │                         │   Authorization: Bearer xxx
      │                 │                         │
      │ ⑩ 返回受保护数据                          │
      │◀───────────────────────────────────────────│

8. 安全机制

机制 说明
Authorization Code 临时凭证,一次性使用,10分钟过期
Client Secret 客户端身份验证,确保只有合法客户端能获取 token
Bearer Token 每个请求携带,1小时过期
State 参数 防止 CSRF 攻击(可选)
Localhost 回调 code 不经过网络传输,防止拦截
Scope 控制 细粒度权限控制(mcp:tools)

MCP TypeScript SDK的使用

作者 wusfe
2026年4月8日 18:18

MCP TypeScript SDK 架构

概述

MCP (Model Context Protocol) TypeScript SDK 实现了模型上下文协议,用于在 LLM 和外部数据源/工具之间建立标准化的通信。


架构图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              MCP Architecture                                │
└─────────────────────────────────────────────────────────────────────────────┘

                              ┌─────────────────┐
                              │   LLM (AI)      │
                              └────────┬────────┘
                                       │
                          ┌────────────▼────────────┐
                          │      MCP Client         │
                          │  @modelcontextprotocol/ │
                          │      sdk/client         │
                          └────────────┬────────────┘
                                       │
                    ┌──────────────────┼──────────────────┐
                    │                  │                  │
          ┌─────────▼─────────┐ ┌──────▼──────┐ ┌───────▼──────┐
          │    Tools API      │ │ Resources   │ │   Prompts    │
          │  listTools()      │ │ listResources│ │ listPrompts()│
          │  callTool()       │ │ readResource │ │  getPrompt()  │
          └─────────┬─────────┘ └──────┬──────┘ └───────┬──────┘
                    │                   │                │
          ┌─────────▼───────────────────▼────────────────▼─────────┐
          │                    Transport Layer                     │
          │  ┌──────────────┐  ┌──────────────────┐  ┌──────────┐ │
          │  │    Stdio     │  │ Streamable HTTP  │  │   SSE    │ │
          │  │  (Local)     │  │   (Remote)       │  │(Legacy)  │ │
          │  └──────────────┘  └──────────────────┘  └──────────┘ │
          └──────────────────────────┬──────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
          ┌─────────▼─────────┐         ┌─────────▼─────────┐
          │   MCP Server      │         │   MCP Client      │
          │  (Provider)       │◄───────►│  (Consumer)       │
          └─────────┬─────────┘   RPC   └───────────────────┘
                    │
          ┌─────────▼─────────────────────────────────────────┐
          │                  Capability Layer                   │
          │  ┌─────────┐  ┌──────────┐  ┌────────┐  ┌───────┐ │
          │  │ Tools   │  │ Resources │  │Prompts │  │Sampling│ │
          │  │         │  │          │  │        │  │Elicit │ │
          │  └─────────┘  └──────────┘  └────────┘  └───────┘ │
          └────────────────────────────────────────────────────┘

组件说明

组件 作用 职责说明
LLM 消费方 大语言模型,通过 MCP Client 发现和使用外部工具、资源。解析用户意图,决定调用哪些工具,处理返回结果
MCP Client 连接管理 SDK 客户端库,负责:① 与服务器建立连接 ② 发现并列出可用工具/资源/提示器 ③ 调用工具并传递参数 ④ 读取资源内容 ⑤ 处理服务器通知
MCP Server 能力提供 SDK 服务器端,负责:① 注册和暴露 Tools(可执行函数)② 注册和暴露 Resources(可读数据)③ 注册和暴露 Prompts(提示模板)④ 处理客户端请求并返回结果
Transport 通信通道 传输层,负责客户端与服务器之间的消息传递。支持多种传输方式:① Stdio - 本地进程间通信 ② HTTP Streamable - 远程 HTTP 通信,支持双向流 ③ SSE - 服务器单向推送事件
Tools 执行能力 服务器暴露的可调用函数,用于执行具体操作(如计算、查询、创建等)。带输入参数,返回执行结果
Resources 数据暴露 服务器暴露的只读数据,支持客户端读取和订阅变更。可以是配置文件、数据库记录、文件内容等
Prompts 模板复用 预定义的提示模板,支持参数化。客户端可以获取填充参数后的完整消息,用于标准化常见任务的提示
Sampling LLM 代理 允许服务器向客户端请求 LLM 推理能力。服务器可以请客户端的 LLM 生成文本、总结内容等
Elicitation 交互代理 允许服务器向客户端请求用户输入。用于需要用户确认、填写表单或做出选择的场景

SDK 模块结构

本项目使用 v1 版本的 @modelcontextprotocol/sdk 统一包。

模块 导入路径 用途
Client @modelcontextprotocol/sdk/client/index.js MCP 客户端实现,连接服务器、调用工具、读取资源
Server @modelcontextprotocol/sdk/server/mcp.js MCP 服务器实现,暴露工具、资源、提示器

协议层设计

Transport 层

Transport 层负责客户端与服务器之间的底层通信。

传输类型 适用场景
Stdio StdioServerTransport / StdioClientTransport 本地进程通信,同一系统内 spawn 子进程
HTTP Streamable StreamableHTTPClientTransport 远程服务器,支持 SSE 事件流(推荐)
SSE SSEClientTransport Server-Sent Events 单向事件流
Stdio 传输

适合本地场景,服务器作为子进程启动:

// 服务器
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const transport = new StdioServerTransport();
await server.connect(transport);

// 客户端
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({ command: 'node', args: ['server.js'] });
HTTP Streamable 传输

适合远程场景,服务器独立运行:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
await client.connect(transport);

Capability 层

MCP 协议定义了四种主要能力。

Tools(工具)

工具是服务器暴露的可调用函数,LLM 可以通过工具执行实际操作。

服务器注册

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod';

server.registerTool(
  'calculate-bmi',
  {
    title: 'BMI Calculator',
    description: 'Calculate Body Mass Index',
    inputSchema: z.object({
      weightKg: z.number().describe('Weight in kilograms'),
      heightM: z.number().describe('Height in meters')
    }),
    outputSchema: z.object({ bmi: z.number() })
  },
  async ({ weightKg, heightM }) => {
    const bmi = weightKg / (heightM * heightM);
    return {
      content: [{ type: 'text', text: `BMI: ${bmi.toFixed(2)}` }],
      structuredContent: { bmi }
    };
  }
);

客户端调用

const result = await client.callTool({
  name: 'calculate-bmi',
  arguments: { weightKg: 70, heightM: 1.75 }
});

Resources(资源)

资源是服务器暴露的二进制或文本数据,客户端可以读取和订阅。

服务器注册

server.registerResource(
  'config://app',
  {
    title: 'App Config',
    description: 'Application configuration',
    mimeType: 'application/json'
  },
  async () => ({
    contents: [{
      uri: 'config://app',
      text: JSON.stringify({ theme: 'dark', language: 'en' })
    }]
  })
);

客户端操作

// 列出资源
const { resources } = await client.listResources();

// 读取资源
const { contents } = await client.readResource({ uri: 'config://app' });

// 订阅变更
await client.subscribeResource({ uri: 'config://app' });
client.setNotificationHandler('notifications/resources/updated', handler);

Prompts(提示器)

提示器是预定义的提示模板,可以携带参数生成消息。

服务器注册

server.registerPrompt(
  'review-code',
  {
    title: 'Code Review',
    description: 'Review code for best practices',
    argsSchema: z.object({ code: z.string() })
  },
  ({ code }) => ({
    messages: [{
      role: 'user',
      content: { type: 'text', text: `Please review:\n\n${code}` }
    }]
  })
);

客户端调用

const { messages } = await client.getPrompt({ name: 'review-code', arguments: { code: '...' } });

Sampling(采样)

Sampling 允许服务器向客户端请求 LLM 推理。

服务器端请求

server.registerTool(
  'summarize',
  { inputSchema: z.object({ text: z.string() }) },
  async ({ text }, ctx) => {
    const response = await ctx.mcpReq.requestSampling({
      messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }],
      maxTokens: 500
    });
    return { content: [{ type: 'text', text: JSON.stringify(response.content) }] };
  }
);

客户端处理

client.setRequestHandler('sampling/createMessage', async request => {
  const lastMessage = request.params.messages.at(-1);
  // 转发给 LLM 并返回
  return { model: 'claude-3-5-sonnet', role: 'assistant', content: { type: 'text', text: '...' } };
});

Elicitation(请求用户输入)

Elicitation 允许服务器向客户端请求用户交互式输入。

服务器端请求

const result = await ctx.mcpReq.elicitInput({
  mode: 'form',
  message: 'Please rate this:',
  requestedSchema: {
    type: 'object',
    properties: { rating: { type: 'integer', minimum: 1, maximum: 5 } },
    required: ['rating']
  }
});

客户端处理

client.setRequestHandler('elicitation/create', async request => ({
  action: 'accept',
  content: { rating: 5 }
}));

Auth 层

SDK 支持 OAuth 2.0 认证。

Provider 用途
ClientCredentialsProvider 客户端凭证流,适用于服务间认证
PrivateKeyJwtProvider 私钥 JWT,适用于更安全的场景
import { ClientCredentialsProvider } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const authProvider = new ClientCredentialsProvider({
  clientId: 'my-service',
  clientSecret: 'my-secret'
});

const transport = new StreamableHTTPClientTransport(
  new URL('http://localhost:3000/mcp'),
  { authProvider }
);

错误处理

SDK 提供 ProtocolErrorSdkError

import { ProtocolError } from '@modelcontextprotocol/sdk/client/index.js';

if (error instanceof ProtocolError) {
  console.log(error.code); // 错误码
  console.log(error.message); // 错误消息
}
❌
❌