OAuth桥接服务

mcp-server-bridge是一个配置驱动的OAuth 2.1桥接服务,用于在任何OAuth 2.0提供商上构建MCP服务器,提供PKCE、动态客户端注册和自动令牌刷新等功能。

  • by CooperNiebuhr
  • TypeScript
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 CooperNiebuhr/mcp-oauth-bridge

使用说明

mcp服务器网桥

配置驱动的OAuth 2.1网桥,用于在任何OAuth 2.0提供程序上构建MCP服务器。

提供一个描述提供者OAuth端点的单一配置对象,您将获得一个完全兼容的 模型上下文协议 具有PKCE、动态客户端注册、每个用户凭据隔离、自动令牌刷新和结构化错误处理的服务器——没有样板。

运作原理

 +-----------+                +------------------+              +--------------+
 |MCP Client |                |mcp-server-bridge |              | Provider API |
 +-----+-----+                +--------+---------+              +------+-------+
       |                               |                               |
       |  (A) Authorization            |                               |
       |                               |                               |
       | ---  authorize (PKCE)  -----> |                               |
       |                               | ---  redirect to login  ----> |
       |                               | <--  auth code  ------------- |
       |                               | ---  exchange code  --------> |
       |                               | <--  provider tokens  ------- |
       | <--  MCP access token  ------ |                               |
       |                               |                               |
       |  (B) Tool Execution           |                               |
       |                               |                               |
       | ---  tool call (Bearer) ----> |                               |
       |                               | ---  authenticated req  ----> |
       |                               | <--  API response  ---------  |
       | <--  tool result  ----------- |                               |
       |                               |                               |

该桥梁在MCP SDK的OAuth 2.1要求和提供商的OAuth 2.0流之间进行转换。您的代码仅定义了提供程序配置和工具处理程序。

入门指南

npm install mcp-server-bridge @modelcontextprotocol/sdk

1.定义您的提供商配置

// src/provider.config.ts
import type { ProviderConfig } from 'mcp-server-bridge';

export const config: ProviderConfig = {
  name: 'HubSpot',

  auth: {
    authorizeUrl: 'https://app.hubspot.com/oauth/authorize',
    tokenUrl: 'https://api.hubapi.com/oauth/v1/token',
    scopes: ['crm.objects.contacts.read'],
  },

  env: {
    clientId: 'HUBSPOT_CLIENT_ID',     // env var name, not the value
    clientSecret: 'HUBSPOT_CLIENT_SECRET',
  },

  callbackPathSegment: 'hubspot',      // → /oauth/hubspot/callback
  apiBaseUrl: 'https://api.hubapi.com',

  async fetchUserIdentity(accessToken) {
    const res = await fetch('https://api.hubapi.com/oauth/v1/access-tokens/' + accessToken);
    if (!res.ok) throw new Error('Failed to fetch user info');
    const data = await res.json() as { user_id: string; user: string };
    return { userId: String(data.user_id), email: data.user };
  },

  mcpServer: { name: 'hubspot', version: '1.0.0' },
};

2.注册您的工具

// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ProviderApiClientInterface } from 'mcp-server-bridge';
import { z } from 'zod';
import { formatToolError } from 'mcp-server-bridge';

export function createServer(client: ProviderApiClientInterface): McpServer {
  const server = new McpServer({ name: 'hubspot', version: '1.0.0' });

  server.tool(
    'hubspot_list_contacts',
    'List contacts from HubSpot CRM',
    { limit: z.number().optional().describe('Max results (default 10)') },
    async ({ limit }) => {
      try {
        const data = await client.request<{ results: unknown[] }>(
          '/crm/v3/objects/contacts',
          { limit: String(limit || 10) },
        );
        return { content: [{ type: 'text', text: JSON.stringify(data.results) }] };
      } catch (err) {
        return formatToolError(err);
      }
    },
  );

  return server;
}

3.启动服务器

// src/index.ts
import { createBridgeServer } from 'mcp-server-bridge';
import { config } from './provider.config.js';
import { createServer } from './server.js';

const { start } = createBridgeServer({
  config,
  createMcpServer: (client) => createServer(client),
});

start();

三个文件,你就有了一个生产就绪的MCP服务器。

提供者配置参考

字段 类型 必填 描述
name string 是 人类可读的提供者名称
auth.authorizeUrl string 是 提供商的OAuth授权端点
auth.tokenUrl string 是 提供商的OAuth令牌端点
auth.scopes string[] 是 请求范围
auth.scopeDelimiter string 无 用于在授权URL中加入作用域的分隔符(默认值: ' ' 根据RFC 6749第3.3节)。设置为 ',' 对于像Zoho这样使用逗号分隔作用域的提供者。
auth.tokenContentType 'form' | 'json' 否 令牌交换请求的内容类型(默认值: 'form').设置为 'json' 对于像Notion和Linear这样需要JSON的提供商。
auth.clientAuthMethod 'body' | 'basic' 否 如何在令牌交换中发送客户端凭据(默认值: 'body').设置为 'basic' 对于需要HTTP基本身份验证的提供商(例如Stripe)。
auth.extraAuthorizeParams Record<string, string> 否 用于授权重定向的额外查询参数(例如。 { access_type: 'offline' })
env.clientId string Yes 包含客户端ID的env变量的名称
env.clientSecret string Yes 持有客户端机密的env变量的名称
callbackPathSegment string 是 回调路由的URL段("zoho" → /oauth/zoho/callback)
apiBaseUrl string 是 提供商的API基础URL
fetchUserIdentity (accessToken: string) => Promise<UserIdentity> 是 在OAuth交换后获取用户信息
authorizeUser (identity: UserIdentity) => string | null 否 返回null表示允许,返回错误消息表示拒绝
tokenNeverExpires boolean 否 设置为 true 对于具有非到期令牌的提供商(请参见 提供商兼容性)
refreshTokenUrl string 否 令牌刷新端点(如果不同) tokenUrl
mcpServer { name: string; version: string } 否 MCP服务器元数据
m2m { getProviderCredentials, scopes? } 否 机器到机器配置(请参阅 M2M认证)

提供商兼容性

OAuth提供者以可预测的方式偏离规范。该桥通过配置选项处理常见的变化:

非到期代币

ClickUp、Notion、Linear、Todoist、Figma和Slack等提供商发行永不过期的访问令牌,并且不提供刷新令牌。集 tokenNeverExpires: true 要处理此问题:

{
  tokenNeverExpires: true,
  // The callback handler will accept responses without a refresh_token.
  // Tokens are stored with a far-future expiry.
  // 401 responses throw ProviderAuthError instead of attempting refresh.
}

JSON令牌交换

Notion和Linear等提供商要求令牌交换体为JSON,而不是 application/x-www-form-urlencoded:

{
  auth: {
    tokenContentType: 'json',
    // ...
  },
}

令牌交换的基本身份验证

像Stripe这样的提供商通过HTTP Basic auth标头而不是请求正文发送客户端凭据:

{
  auth: {
    clientAuthMethod: 'basic',
    // ...
  },
}

自定义范围分隔符

大多数提供者根据RFC 6749使用空格分隔的作用域。Zoho使用逗号:

{
  auth: {
    scopeDelimiter: ',',
    scopes: ['ZohoCRM.modules.ALL', 'ZohoCRM.settings.ALL'],
    // ...
  },
}

额外授权参数

一些提供商要求在授权重定向上添加其他参数(例如谷歌的 access_type: 'offline' 获取刷新令牌):

{
  auth: {
    extraAuthorizeParams: { access_type: 'offline', prompt: 'consent' },
    // ...
  },
}

机器对机器身份验证

对于非交互式客户端(CI管道、无头代理),网桥支持 client_credentials 授权类型。使用配置 m2m 选项:

const config: ProviderConfig = {
  // ... standard config ...

  m2m: {
    // Return provider credentials for M2M access.
    // These might come from a service account, a stored token, etc.
    async getProviderCredentials() {
      return {
        accessToken: process.env.SERVICE_ACCOUNT_TOKEN!,
        refreshToken: process.env.SERVICE_ACCOUNT_REFRESH!,
        expiresIn: 3600,
      };
    },
    // Optional: restrict M2M clients to a subset of scopes
    scopes: ['read'],
  },
};

M2M客户端通过POSTing进行身份验证 /token 和 grant_type=client_credentials:

curl -X POST https://your-bridge.example.com/token \
  -d grant_type=client_credentials \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

注: MCP SDK的令牌处理程序不支持 client_credentials 服务器端,因此网桥安装了一个瘦中间件,在SDK处理程序运行之前拦截此授权类型。所有其他补助类型(authorization_code, refresh_token)以不变的方式传递到SDK。

自定义存储后端

默认情况下,网桥使用具有JSON文件持久性的内存映射(通过配置 OAUTH_STORE_PATH).对于需要横向扩展或持久性的生产部署,您可以注入自己的存储:

import { createBridgeServer } from 'mcp-server-bridge';
import type { ClientsStore, TokenStore } from 'mcp-server-bridge';

const myClientsStore: ClientsStore = { /* your Redis/Postgres/DynamoDB impl */ };
const myTokenStore: TokenStore = { /* your Redis/Postgres/DynamoDB impl */ };

const { start } = createBridgeServer({
  config,
  createMcpServer: (client) => createServer(client),
  stores: { clientsStore: myClientsStore, tokenStore: myTokenStore },
});

这 ClientsStore 和 TokenStore 接口从包中导出。这 ClientsStore 接口扩展了MCP SDK OAuthRegisteredClientsStore,因此实现与SDK的注册和身份验证处理程序直接兼容。

记录类型(AuthCodeRecord, AccessTokenRecord, RefreshTokenRecord, PendingAuthRecord)还导出以用于自定义存储实现。

API客户端

每个工具处理程序都会收到一个 ProviderApiClientInterface 有四种方法用于向提供者API发出经过身份验证的请求:

client.request<T>(endpoint, params?)   // GET
client.create<T>(endpoint, body)       // POST
client.update<T>(endpoint, body)       // PUT
client.remove<T>(endpoint, params?)    // DELETE

所有方法自动执行:

  • 包含承载授权标头
  • 刷新401响应上的访问令牌(一次透明重试)
  • 对429个速率限制(1s、2s、4s)实施指数回退
  • 抛出类型错误类(见下文)

错误处理

该桥提供类型化错误类,因此工具可以返回AI代理可以推理的结构化错误:

类 代码 属性 抛出时
ProviderAuthError PROVIDER_AUTH_ERROR -- 令牌交换或刷新失败
ProviderRateLimitError PROVIDER_RATE_LIMIT retryAfter: number | null 429在所有重试尝试均已尝试完毕后
ProviderApiError PROVIDER_API_ERROR statusCode, responseBody 来自提供商的非-2xx响应
ProviderNetworkError PROVIDER_NETWORK_ERROR -- 网络连接失败

使用 formatToolError() 在您的工具捕获块中返回符合MCP的错误响应:

import { formatToolError } from 'mcp-server-bridge';

server.tool('my_tool', 'Does something', {}, async () => {
  try {
    const data = await client.request('/endpoint');
    return { content: [{ type: 'text', text: JSON.stringify(data) }] };
  } catch (err) {
    return formatToolError(err);
  }
});

服务器路由

网桥服务器会自动挂载这些路由:

路线 方法 目的
/.well-known/oauth-authorization-server GET OAuth 2.1元数据发现(启用CORS)
/.well-known/oauth-protected-resource/mcp GET 受保护的资源元数据(启用CORS)
/register POST 动态客户端注册(速率受限)
/authorize GET 授权(重定向到提供商,速率受限)
/token POST 令牌端点(启用CORS,速率受限)
/revoke POST 令牌吊销(启用CORS,速率受限)
/oauth/{provider}/callback GET 提供程序OAuth回调
/health GET 健康检查
/mcp POST MCP传输(受承载令牌保护)

所有OAuth端点都通过MCP SDK的内置处理程序包含速率限制和CORS支持。这 /authorize 处理程序验证重定向URI 《联邦法规》第8252条第7.3款,允许环回地址的任何端口支持本地MCP客户端。

OAuth 2.1合规性

该桥实现了OAuth 2.1和相关规范要求:

  • PKCE(S256) --对所有授权代码流强制执行
  • 动态客户端注册 — RFC 7591
  • 代币轮换 --每次使用刷新令牌时都会轮换
  • 资源指标 — RFC 8707 通过完整的身份验证流程
  • 令牌撤销 — RFC 7009
  • 授权服务器元数据 — RFC 8414
  • 受保护的资源元数据 — RFC 9728
  • 重定向URI验证 — 《联邦法规》第8252条第7.3款 具有环回端口灵活性
  • 令牌响应的范围 — RFC 6749§5.1
  • 提供商令牌生命周期 --MCP令牌刷新验证上游凭据是否仍然存在

环境变量

变量 必填 描述
{PROVIDER}_CLIENT_ID 是 OAuth应用程序客户端ID(配置中设置的var名称)
{PROVIDER}_CLIENT_SECRET 是 OAuth应用程序客户端密钥(配置中设置了var名称)
MCP_OAUTH_ISSUER 是 您的服务器的公共URL(例如。 https://my-mcp.example.com)
OAUTH_STORE_PATH 否 令牌存储文件路径(默认: /data/oauth-store.json).提供定制商店时不使用。
PORT 无 服务器端口(默认值: 3000)

部署

服务器是一个标准的Express应用程序。在Node.js运行的任何地方部署它——铁路、Fly.io、VPS等。

要求:

  • HTTPS正在生产中(OAuth 2.1要求)
  • MCP_OAUTH_ISSUER 必须是公共HTTPS URL
  • 提供商OAuth应用程序的重定向URI必须与您的回调URL匹配

反向代理后面:

const { start } = createBridgeServer({
  config,
  createMcpServer: (client) => createServer(client),
  trustProxy: 1, // Trust one level of proxy (Railway, Nginx, etc.)
});

测试

npm test          # Run all tests
npm run test:watch # Watch mode

测试套件包括OAuth令牌存储、提供程序流、API客户端重试逻辑、令牌管理器缓存和服务器集成(元数据、注册、令牌交换、M2M、承载身份验证)。

许可证

麻省理工学院