1. 项目概述:为什么我们需要为AI开发工具装上“刹车”和“行车记录仪”?

最近在深度使用Cursor这类AI驱动的代码编辑器时,我遇到了一个很实际的问题:团队协作时,如何管理AI助手(比如Cursor内置的Claude或GPT)的调用成本,并清晰地追踪谁在什么时候、为了什么目的调用了它?这听起来像是个管理问题,但其实是个技术活。当你的工具链深度集成了AI能力,每一次代码补全、每一次对话解释,背后都可能是一次API调用,产生实实在在的费用。放任不管,月底的账单可能会让你大吃一惊;而缺乏审计,出了问题(比如生成了不安全的代码)也根本无从追溯。

这就是“Cursor MCP Proxy Setup”这个项目要解决的核心痛点。MCP,即Model Context Protocol,你可以把它理解为一套让不同AI工具和应用之间安全、标准化通信的“普通话”。而“Proxy Setup”就是为这套通信建立一个“中间站”或“网关”。这个网关的核心使命有两个: 预算控制 审计追踪 。想象一下,你给团队的AI工具使用装上了“预算锁”和“黑匣子”,既能防止成本超支,又能记录每一次“飞行数据”。

这个指南适合所有在团队环境中使用Cursor、Claude Desktop或其他支持MCP的AI工具的开发者、技术负责人和DevOps工程师。无论你是想控制个人项目的零星开销,还是需要管理一个几十人团队的AI资源使用,这套方案都能提供一个清晰、可落地的技术路径。接下来,我会拆解整个搭建过程,从原理到实操,并分享我踩过的一些坑和优化技巧。

2. 整体架构与核心组件选型解析

在开始动手之前,我们必须先理解我们要构建的是什么,以及为什么选择这些组件。整个系统的目标是在Cursor(客户端)和AI模型服务(如Anthropic的Claude API)之间,插入一个我们自己掌控的代理服务器。

2.1 为什么是MCP代理?

MCP协议的本质是定义了一套标准的JSON-RPC接口,用于工具(如Cursor)和模型服务器之间交换提示词、上下文和工具调用信息。直接连接时,Cursor会通过MCP协议直接与官方的Claude API服务器对话。而代理模式,则是让Cursor先连接到我们自己的服务器,再由我们的服务器去转发请求到真正的API终点。

这样做的好处显而易见:

  1. 集中控制点 :所有流量都经过我们的服务器,这是实施预算和审计的黄金位置。
  2. 协议透明 :MCP基于HTTP和JSON-RPC,是标准的Web协议,易于用常规的Web技术进行拦截、分析和修改。
  3. 与客户端解耦 :无需修改Cursor或任何客户端的代码,只需改变其配置中的连接地址,对终端用户完全透明。

2.2 核心组件技术选型

我们需要构建两个核心功能模块: 代理转发 控制逻辑 。以下是经过实践验证的选型方案及其理由:

1. 代理服务器框架:Node.js + Express

  • 理由 :MCP通信本质是HTTP,Node.js的异步非阻塞特性非常适合处理大量并发的API转发请求。Express是Node.js生态中最成熟、最灵活的Web框架,中间件机制可以让我们轻松地插入审计日志、速率限制和成本计算逻辑。相较于Python的Flask/FastAPI,Node.js在处理JSON-RPC这种纯HTTP/JSON的流水线作业时,通常更轻量、启动更快。
  • 备选方案 :Go (Gin/Echo框架) 是另一个高性能选择,适合对并发和资源消耗有极致要求的场景,但初期开发速度可能略慢于Node.js。

2. 审计日志存储:SQLite (开发/轻量) 或 PostgreSQL (生产)

  • 理由 :审计数据需要结构化存储以便查询分析。SQLite无需单独部署数据库服务,一个文件搞定,非常适合个人或小团队初期使用。它的简单性让我们能快速搭建原型。当数据量增大或需要团队协作访问时,可以无缝迁移到PostgreSQL。审计日志的关键字段应包括:请求ID、时间戳、用户标识(如API Key或用户名)、模型类型、提示词Token数、完成Token数、估算成本、请求状态和原始请求/响应的摘要或哈希值(注意隐私,可能不存全文)。
  • 注意 :切勿将完整的提示词和响应内容(可能包含敏感代码或业务逻辑)明文存入日志,应只存元数据或进行哈希处理。

3. 预算控制实现:内存存储 + 定期持久化

  • 理由 :预算检查需要极低的延迟和高频的读写(每次API调用前都要检查)。使用内存(如JavaScript的Map或对象)来存储用户/团队的实时预算消耗是最快的。同时,我们需要一个后台进程,定期(例如每5分钟)将内存中的数据快照持久化到上述的SQLite/PostgreSQL中,防止服务器重启导致数据丢失。对于分布式部署,则需要引入Redis等分布式缓存来共享预算状态。
  • 关键设计 :预算检查必须是一个 原子操作 。在高并发下,需要防止“超卖”(两个请求同时读取余额,都判断为足够,然后都扣费导致透支)。在单进程Node.js中,可以利用其单线程事件循环的特性,配合异步队列来简化这个问题。更严谨的做法是使用数据库的行锁或Redis的原子操作(INCRBY/DECRBY)。

4. 用户/团队标识:API Key 体系

  • 理由 :我们需要区分不同用户或团队的流量。最通用的方式是为每个用户或团队生成一个唯一的API Key。Cursor在配置MCP服务器时,可以将这个Key作为连接参数或放在请求头中(如 Authorization: Bearer <api_key> )。我们的代理服务器在收到请求后,首先验证这个Key的有效性,并将其作为预算归属和审计日志的标识。
  • 实操技巧 :API Key可以设计成有不同权限等级(如只读、标准、管理员),并可以设置启用/禁用状态。Key的生成可以使用 crypto.randomBytes 生成高强度随机字符串,并哈希后存储,仅在一次性地将明文Key返回给用户。

3. 分步搭建MCP代理服务器

理论清晰后,我们开始动手搭建。我将以Node.js + Express + SQLite的技术栈为例,展示从零到一的构建过程。

3.1 初始化项目与依赖安装

首先,创建一个新的项目目录并初始化。

mkdir cursor-mcp-proxy
cd cursor-mcp-proxy
npm init -y

安装核心依赖:

npm install express dotenv axios sqlite3 bcryptjs jsonwebtoken
npm install --save-dev nodemon
  • express : Web服务器框架。
  • dotenv : 管理环境变量(如数据库路径、API密钥、预算限额)。
  • axios : 用于向真实的AI API端点(如 api.anthropic.com )转发请求。
  • sqlite3 : 操作SQLite数据库。
  • bcryptjs : 用于哈希API Key(如果存数据库)。
  • jsonwebtoken : 可选项,用于生成和验证更复杂的JWT Token作为身份凭证。
  • nodemon : 开发工具,代码变动时自动重启服务器。

创建项目基础结构:

cursor-mcp-proxy/
├── .env
├── .gitignore
├── package.json
├── server.js          # 主入口文件
├── config/
│   └── index.js       # 配置管理
├── middleware/
│   ├── auth.js        # API Key认证中间件
│   ├── audit.js       # 审计日志中间件
│   └── budget.js      # 预算检查中间件
├── services/
│   ├── db.js          # 数据库连接与初始化
│   ├── budgetService.js # 预算管理逻辑
│   └── auditService.js  # 审计日志逻辑
└── routes/
    └── mcp-proxy.js   # MCP代理路由

3.2 配置管理与数据库初始化

.env 文件中配置关键信息:

PORT=3000
NODE_ENV=development
DB_PATH=./data/audit.db
ANTHROPIC_API_KEY=your_actual_anthropic_api_key_here
ANTHROPIC_BASE_URL=https://api.anthropic.com
DEFAULT_BUDGET_MONTHLY_USD=50.00 # 默认月度预算(美元)

config/index.js 中集中管理配置:

require('dotenv').config();

module.exports = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  dbPath: process.env.DB_PATH,
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
  anthropicBaseUrl: process.env.ANTHROPIC_BASE_URL,
  defaultBudget: parseFloat(process.env.DEFAULT_BUDGET_MONTHLY_USD) || 50.0,
};

services/db.js 中初始化SQLite数据库和审计表:

const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const config = require('../config');

let db;

function initDatabase() {
  const dbPath = path.resolve(__dirname, '..', config.dbPath);
  // 确保数据目录存在
  const fs = require('fs');
  const dir = path.dirname(dbPath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }

  db = new sqlite3.Database(dbPath, (err) => {
    if (err) {
      console.error('Could not connect to database', err);
    } else {
      console.log('Connected to SQLite database.');
      createTables();
    }
  });
}

function createTables() {
  // 审计日志表
  db.run(`CREATE TABLE IF NOT EXISTS audit_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    request_id TEXT NOT NULL,
    api_key_id TEXT NOT NULL, -- 关联的API Key标识
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    model TEXT,
    prompt_tokens INTEGER,
    completion_tokens INTEGER,
    estimated_cost_usd REAL,
    status_code INTEGER,
    path TEXT,
    user_agent TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )`);

  // API Key管理表(简化版)
  db.run(`CREATE TABLE IF NOT EXISTS api_keys (
    id TEXT PRIMARY KEY, -- 例如:key_abc123
    hashed_key TEXT NOT NULL, -- 存储哈希值,非明文
    name TEXT,
    monthly_budget_usd REAL DEFAULT 50.00,
    current_spent_usd REAL DEFAULT 0.00,
    reset_date DATE, -- 预算重置日期,如每月1号
    is_active BOOLEAN DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )`);

  // 预算快照表(用于持久化内存中的实时花费)
  db.run(`CREATE TABLE IF NOT EXISTS budget_snapshots (
    api_key_id TEXT NOT NULL,
    snapshot_date DATE NOT NULL,
    spent_usd REAL NOT NULL,
    PRIMARY KEY (api_key_id, snapshot_date)
  )`);
}

function getDb() {
  if (!db) {
    initDatabase();
  }
  return db;
}

module.exports = {
  initDatabase,
  getDb,
};

注意 :这里存储的是API Key的哈希值,而不是明文。当客户端传来API Key时,我们需要用同样的算法哈希后与数据库中的 hashed_key 比对。 bcryptjs 库非常适合做这个。永远不要在日志或响应中泄露明文API Key。

3.3 实现核心中间件:认证、审计与预算

中间件是Express处理请求的管道,我们将功能模块化。

1. 认证中间件 ( middleware/auth.js ):

const bcrypt = require('bcryptjs');
const { getDb } = require('../services/db');

async function authenticateApiKey(req, res, next) {
  // 从请求头中获取API Key,例如:Authorization: Bearer sk_proxy_abc123
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: { message: 'Missing or invalid Authorization header' } });
  }

  const apiKey = authHeader.substring(7); // 去掉'Bearer '前缀
  const db = getDb();

  // 这里简化处理:实际应查询数据库,比对哈希值
  // 为了演示,我们假设有一个内存中的有效Key映射
  // 实际项目中,这里应该查询 `api_keys` 表,用 bcrypt.compareSync(apiKey, hashedKey)
  const isValidKey = await validateApiKeyFromDb(apiKey);

  if (!isValidKey) {
    return res.status(403).json({ error: { message: 'Invalid API key' } });
  }

  // 将验证后的Key信息附加到请求对象,供后续中间件使用
  req.apiKeyId = isValidKey.id; // 假设返回对象包含id
  req.apiKeyBudget = isValidKey.monthly_budget_usd;
  req.apiKeySpent = isValidKey.current_spent_usd;
  next();
}

// 模拟数据库验证函数
async function validateApiKeyFromDb(apiKey) {
  // 实际应从数据库查询并比对哈希
  // 此处返回模拟数据
  return {
    id: 'team_dev',
    monthly_budget_usd: 100.00,
    current_spent_usd: 23.50,
  };
}

module.exports = { authenticateApiKey };

2. 预算检查中间件 ( middleware/budget.js ):

// 内存中的预算缓存,键为 apiKeyId,值为已花费金额(美元)
const budgetCache = new Map();

function checkBudget(req, res, next) {
  const { apiKeyId, apiKeyBudget } = req;
  const estimatedCost = req.estimatedCost || 0; // 这个值需要在审计中间件中计算并附加

  if (!apiKeyId) {
    return next(new Error('API Key信息缺失,请先通过认证中间件'));
  }

  const currentSpent = budgetCache.get(apiKeyId) || 0;
  const projectedSpent = currentSpent + estimatedCost;

  // 检查是否超预算
  if (projectedSpent > apiKeyBudget) {
    return res.status(429).json({
      error: {
        message: `Budget exceeded. Monthly budget: $${apiKeyBudget}, already spent: $${currentSpent.toFixed(2)}, this request would cost ~$${estimatedCost.toFixed(2)}.`,
        type: 'budget_limit'
      }
    });
  }

  // 预算充足,将预估成本暂存,待请求成功后再扣减
  req.projectedCost = estimatedCost;
  next();
}

// 一个简单的函数,用于在请求成功后更新内存缓存
function updateBudgetCache(apiKeyId, cost) {
  const current = budgetCache.get(apiKeyId) || 0;
  budgetCache.set(apiKeyId, current + cost);
}

// 定期将内存缓存持久化到数据库的函数(需另设定时任务)
async function syncBudgetToDb() {
  // ... 遍历 budgetCache,更新 api_keys 表的 current_spent_usd 字段
}

module.exports = { checkBudget, updateBudgetCache, budgetCache };

3. 审计日志中间件 ( middleware/audit.js ): 这是最复杂的部分,它需要拦截请求和响应,计算成本,并记录日志。

const { getDb } = require('../services/db');
const { v4: uuidv4 } = require('uuid'); // 需要安装 uuid 包

async function auditLog(req, res, next) {
  const startTime = Date.now();
  const requestId = uuidv4();
  req.requestId = requestId;

  // 捕获原始响应发送方法
  const originalSend = res.send;
  let responseBody;

  res.send = function(body) {
    responseBody = body;
    originalSend.call(this, body);
  };

  // 响应完成后记录日志
  res.on('finish', async () => {
    const duration = Date.now() - startTime;
    const { apiKeyId, path, method } = req;
    const userAgent = req.get('User-Agent') || '';

    // 解析请求体,估算Token和成本(简化版)
    let estimatedCost = 0;
    let model = 'unknown';
    let promptTokens = 0;
    let completionTokens = 0;

    try {
      // MCP请求体通常是JSON RPC格式,我们需要解析其中的参数
      if (req.body && req.body.params && req.body.params.messages) {
        // 这是一个非常粗略的估算!实际应根据模型定价和准确的Token数计算。
        // 例如,Claude 3 Opus: $15 / 1M input tokens, $75 / 1M output tokens
        model = req.body.params.model || 'claude-3-opus-20240229';
        // 假设我们有一个函数 estimateTokens 来估算消息的token数
        promptTokens = estimateTokens(req.body.params.messages);
        // 输出Token数需要从响应体中获取
        if (responseBody && typeof responseBody === 'string') {
          const resp = JSON.parse(responseBody);
          if (resp.result && resp.result.content) {
            completionTokens = estimateTokens(resp.result.content);
          }
        }
        // 简单成本计算(示例价格,需替换为实际)
        const inputCostPerMillion = 15.0; // $15 per 1M input tokens
        const outputCostPerMillion = 75.0; // $75 per 1M output tokens
        estimatedCost = (promptTokens / 1_000_000) * inputCostPerMillion +
                       (completionTokens / 1_000_000) * outputCostPerMillion;
      }
    } catch (err) {
      console.error('Failed to estimate cost:', err);
    }

    // 将估算的成本附加到请求对象,供预算中间件使用(注意:这是响应后,预算检查在之前)
    // 更合理的架构是在转发请求前,根据请求内容预先估算一个成本用于预算检查。
    // 这里为了流程清晰,先记录。

    // 记录到数据库
    const db = getDb();
    const stmt = db.prepare(`
      INSERT INTO audit_logs 
      (request_id, api_key_id, model, prompt_tokens, completion_tokens, estimated_cost_usd, status_code, path, user_agent)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);
    stmt.run(
      requestId,
      apiKeyId || 'unknown',
      model,
      promptTokens,
      completionTokens,
      estimatedCost,
      res.statusCode,
      path,
      userAgent
    );
    stmt.finalize();

    // 如果请求成功,更新内存中的预算缓存
    if (res.statusCode >= 200 && res.statusCode < 300 && apiKeyId && estimatedCost > 0) {
      const { updateBudgetCache } = require('./budget');
      updateBudgetCache(apiKeyId, estimatedCost);
    }

    console.log(`[Audit] ${method} ${path} - ${res.statusCode} - ${duration}ms - Cost: ~$${estimatedCost.toFixed(4)}`);
  });

  next();
}

// 一个非常粗略的Token估算函数(实际应用应使用tiktoken库或模型提供商提供的SDK)
function estimateTokens(textOrMessages) {
  // 简化:按字符数除以4估算(英文近似)。中文等语言不同。
  let totalChars = 0;
  if (Array.isArray(textOrMessages)) {
    textOrMessages.forEach(msg => {
      if (msg.content) totalChars += String(msg.content).length;
    });
  } else if (typeof textOrMessages === 'string') {
    totalChars = textOrMessages.length;
  }
  return Math.ceil(totalChars / 4);
}

module.exports = { auditLog };

3.4 构建代理路由与主服务器

现在,我们将中间件和转发逻辑组合起来。

代理路由 ( routes/mcp-proxy.js ):

const express = require('express');
const axios = require('axios');
const { authenticateApiKey } = require('../middleware/auth');
const { checkBudget } = require('../middleware/budget');
const { auditLog } = require('../middleware/audit');
const config = require('../config');

const router = express.Router();

// 关键:解析JSON请求体。MCP使用JSON-RPC over HTTP。
router.use(express.json());

// 应用中间件链:认证 -> 审计(记录开始)-> 预算检查 -> 转发 -> 审计(记录结束)
router.all('*', authenticateApiKey, auditLog, checkBudget, async (req, res) => {
  try {
    // 构建转发到真实Anthropic API的请求
    const targetUrl = `${config.anthropicBaseUrl}${req.path}`;
    const headers = {
      'Content-Type': 'application/json',
      'x-api-key': config.anthropicApiKey, // 使用我们自己的Anthropic主Key
      'anthropic-version': '2023-06-01', // 根据实际情况调整
      // 可以选择性传递一些客户端头
      ...(req.headers['user-agent'] && { 'User-Agent': req.headers['user-agent'] }),
    };

    // 转发请求
    const response = await axios({
      method: req.method,
      url: targetUrl,
      headers: headers,
      data: req.body,
      // 可以设置超时等参数
      timeout: 120000, // 120秒
    });

    // 将响应返回给客户端(如Cursor)
    res.status(response.status).json(response.data);
  } catch (error) {
    console.error('Proxy error:', error.message);
    // 处理错误,将上游错误信息适当返回给客户端
    const status = error.response?.status || 500;
    const message = error.response?.data?.error?.message || error.message;
    res.status(status).json({
      error: {
        type: 'proxy_error',
        message: `Proxy request failed: ${message}`,
      }
    });
  }
});

module.exports = router;

主服务器文件 ( server.js ):

const express = require('express');
const config = require('./config');
const { initDatabase } = require('./services/db');
const mcpProxyRouter = require('./routes/mcp-proxy');

// 初始化数据库
initDatabase();

const app = express();
const PORT = config.port;

// 全局中间件(可选)
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next();
});

// 挂载MCP代理路由。所有发往 /v1/ 的请求(这是Anthropic API的典型路径)都由代理处理。
app.use('/v1', mcpProxyRouter);

// 健康检查端点
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// 一个简单的管理端点,查看当前预算缓存(生产环境需要加权限!)
app.get('/admin/budget-cache', (req, res) => {
  const { budgetCache } = require('./middleware/budget');
  res.json(Object.fromEntries(budgetCache));
});

app.listen(PORT, () => {
  console.log(`Cursor MCP Proxy Server running on http://localhost:${PORT}`);
  console.log(`MCP endpoint: http://localhost:${PORT}/v1`);
});

package.json 中添加启动脚本:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

现在,运行 npm run dev ,你的MCP代理服务器就在 http://localhost:3000 上运行了。

4. 配置Cursor客户端连接代理

服务器搭好了,现在需要告诉Cursor去使用它。Cursor通过其配置文件来定义MCP服务器。

  1. 找到Cursor配置 :Cursor的配置通常位于用户目录下的一个JSON文件中。例如,在macOS上,路径可能是 ~/Library/Application Support/Cursor/User/globalStorage/mcp.json 或通过Cursor的设置界面进行配置。请查阅Cursor的最新文档确认。
  2. 编辑MCP配置 :你需要添加或修改一个MCP服务器配置,将其指向你的代理服务器。配置可能如下所示:
{
  "mcpServers": {
    "my-anthropic-proxy": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-anthropic",
        "--api-key",
        "sk_proxy_abc123def456", // 这是你代理服务器颁发的API Key,不是Anthropic的
        "--api-url",
        "http://localhost:3000/v1" // 指向你的代理服务器
      ]
    }
  }
}

关键点

  • command args 是启动MCP服务器的命令。这里我们假设使用一个官方的或社区的Anthropic MCP服务器实现,它通过命令行参数接收API Key和自定义URL。
  • --api-key 参数的值,应该是你从 自己的代理服务器 管理后台生成的Key(如 sk_proxy_xxx ),而不是原始的Anthropic API Key。你的代理服务器会验证这个Key。
  • --api-url 参数至关重要,它告诉这个MCP服务器客户端(即Cursor启动的进程)将请求发送到你的代理地址( localhost:3000/v1 ),而不是默认的 api.anthropic.com
  1. 重启Cursor :保存配置后,完全重启Cursor,使其加载新的MCP服务器设置。

现在,当你在Cursor中使用AI功能时,请求流将变为: Cursor -> 本地MCP服务器进程 -> 你的代理服务器( localhost:3000 ) -> 真实的Anthropic API 。你的代理服务器完成了认证、审计和预算检查的全流程。

5. 生产环境部署与进阶优化

本地开发环境跑通只是第一步。要服务于团队,你需要考虑生产部署。

5.1 部署方案选择

  • 传统VPS/云服务器 :在DigitalOcean、AWS EC2、Google Cloud Compute Engine等上部署。你需要:
    • 配置Node.js环境。
    • 使用 pm2 systemd 管理进程,保证服务持续运行。
    • 配置Nginx或Apache作为反向代理,处理SSL/TLS加密(HTTPS)。 非常重要 :Cursor等客户端很可能要求HTTPS连接。
    • 绑定域名,并配置DNS。
  • 容器化部署(推荐) :使用Docker将你的代理服务器封装成镜像。
    # Dockerfile
    FROM node:18-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    EXPOSE 3000
    USER node
    CMD ["node", "server.js"]
    
    然后可以在任何支持Docker的环境(如自有服务器、Kubernetes集群)中运行,或者使用云服务商的容器托管服务(如AWS ECS、Google Cloud Run、Azure Container Instances)。这种方式环境一致,易于扩展。
  • Serverless函数 :将代理逻辑拆分为函数(如AWS Lambda + API Gateway)。这对于突发流量成本优化有好处,但需要重新架构,将状态(如内存预算缓存)存储到外部服务(如Redis),复杂度较高。

5.2 安全性强化

  1. HTTPS是必须的 :使用Let‘s Encrypt免费证书或云服务商提供的证书,通过Nginx配置SSL。
  2. API Key管理 :实现一个简单的管理界面(或命令行工具)来生成、吊销、查看API Key及其使用情况。Key应使用 bcrypt 等强哈希算法存储。
  3. 请求速率限制 :使用 express-rate-limit 等中间件,防止单个Key滥用。
  4. 输入验证与过滤 :虽然你是代理,但也应对转发的请求体做基本检查,防止注入攻击或异常请求。
  5. 日志脱敏 :确保审计日志不记录完整的提示词和响应,尤其是可能包含密码、密钥、个人信息的对话。

5.3 预算与审计功能增强

  • 预算重置策略 :实现按周期(月、周)自动重置预算。可以在 api_keys 表中设置 reset_date ,并创建一个每日运行的定时任务检查是否需要重置(将 current_spent_usd 归零,并更新 reset_date )。
  • 实时成本估算 :在 checkBudget 中间件中,你需要一个更准确的 预扣费 估算。可以解析请求中的 model messages ,使用对应模型的定价和Token估算库(如 @anthropic-ai/tokenizer for Claude, tiktoken for GPT)进行快速估算。预扣费成功后,请求转发;请求完成后,根据实际返回的Token数(从响应头或响应体获取)进行 结算 ,多退少补(调整内存缓存和数据库)。
  • 审计仪表盘 :构建一个简单的Web页面,让团队成员可以查看自己的使用量、成本趋势、常用模型等。可以使用Chart.js等库可视化数据。
  • 告警机制 :当预算使用达到80%、90%、100%时,通过邮件、Slack或钉钉发送通知。

5.4 性能与可靠性

  • 连接池与超时 :配置 axios 的HTTP Agent,复用到底层API的连接,提升性能。设置合理的超时(如连接超时、响应超时)。
  • 错误重试 :对于上游API(Anthropic)的瞬时失败(5xx错误),可以实现指数退避的重试机制。
  • 高可用 :如果团队规模大,考虑部署多个代理实例,前面用负载均衡器(如Nginx, HAProxy)分发流量。此时预算缓存必须使用共享存储,如Redis。

6. 常见问题与故障排查实录

在实际搭建和运行过程中,你几乎一定会遇到下面这些问题。这里是我的排查笔记。

Q1: Cursor连接代理失败,提示“无法连接到MCP服务器”或“认证失败”。

  • 检查步骤
    1. 代理服务器是否在运行? curl http://localhost:3000/health 看是否返回 {“status”:”ok”}
    2. Cursor配置的URL和端口是否正确? 确认 --api-url 参数指向了正确的地址。如果是远程服务器,确保是HTTPS且域名可解析。
    3. 防火墙/安全组规则 :确保服务器防火墙(如 ufw )或云服务商安全组开放了代理服务器的端口(如3000)。
    4. 认证中间件日志 :查看代理服务器的控制台输出,看认证中间件是否报错。检查API Key的哈希比对逻辑。
    5. MCP服务器命令路径 :确保Cursor配置中 command 指定的命令(如 npx )在Cursor的运行环境中可用。

Q2: 请求能转发,但Anthropic API返回403或401错误。

  • 原因 :你的代理服务器没有正确地将自己的Anthropic主API Key设置到转发请求的头部。
  • 排查 :在代理服务器的转发代码中,打印出即将发送的请求头(注意不要打印出真实的Key)。确保 x-api-key 头被正确设置,且其值是你的有效Anthropic API Key。检查Key是否有调用对应模型的权限。

Q3: 预算控制不准确,感觉扣费比实际多或少。

  • 原因 :Token估算不准或成本计算模型不对。
  • 解决
    • 使用官方Tokenizer :放弃简单的字符数估算,集成Anthropic官方提供的Token计算库(如Javascript版的 @anthropic-ai/tokenizer ),在审计中间件中精确计算Prompt Tokens。
    • 从响应头获取实际用量 :Anthropic API的响应头通常包含 anthropic-input-tokens anthropic-output-tokens 字段。用这个实际值来计算成本,比估算准确得多。在审计日志的 res.on(‘finish’) 回调中,你可以访问 res.get(‘anthropic-input-tokens’) 来获取。
    • 更新定价表 :定期检查Anthropic官网的定价页面,及时更新代码中的 inputCostPerMillion outputCostPerMillion 变量。

Q4: 服务器重启后,内存中的预算缓存清零了。

  • 原因 :如设计所述,内存缓存是易失的。
  • 解决 :实现 syncBudgetToDb 函数,并设置一个定时任务(例如使用 node-cron 库),每1分钟或5分钟将 budgetCache 中的数据同步到数据库的 api_keys 表的 current_spent_usd 字段。服务器启动时,从数据库读取各API Key的 current_spent_usd 来初始化 budgetCache

Q5: 高并发下,出现了预算超支(两个请求同时通过检查)。

  • 原因 :预算检查不是原子操作。
  • 解决(单机版) :可以利用Node.js单线程特性,将预算检查与扣减逻辑放入一个异步队列(如 async/await 配合一个全局的Promise锁),确保同一API Key的请求串行处理预算。但这会影响性能。
  • 解决(推荐) :引入Redis,使用Redis的 INCRBY 命令的原子性。伪代码如下:
    const currentSpent = await redisClient.incrByFloat(`budget:${apiKeyId}`, estimatedCost);
    if (currentSpent > budgetLimit) {
      // 如果超了,需要回滚刚才的增加
      await redisClient.incrByFloat(`budget:${apiKeyId}`, -estimatedCost);
      return res.status(429).json(...);
    }
    // 预算充足,继续处理请求
    
    这确保了检查和扣减是一个不可分割的操作。

搭建这样一个MCP代理,初期可能会觉得繁琐,但一旦运行起来,它带来的成本可见性和控制力是巨大的。它不仅是财务上的“刹车”,更是技术管理上的“仪表盘”。你可以清楚地看到哪个团队、哪个项目、哪种类型的任务消耗了最多的AI资源,从而优化使用策略,让宝贵的AI算力花在刀刃上。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐