AI开发成本管控实战:基于Node.js与SQLite构建Cursor MCP代理服务器
1. 项目概述为什么我们需要为AI开发工具装上“刹车”和“行车记录仪”最近在深度使用Cursor这类AI驱动的代码编辑器时我遇到了一个非常现实的问题成本失控。Cursor通过集成的模型服务比如GPT-4、Claude等提供了强大的代码生成和补全能力这极大地提升了开发效率。但随之而来的是API调用费用像脱缰的野马一样难以预测。团队里谁在什么时候调用了什么模型、消耗了多少Token、花了多少钱完全是一笔糊涂账。更不用说有时一个不小心写错的循环或者一个过于“热情”的自动补全提示就可能触发一连串的API请求月底看到账单时只能倒吸一口凉气。这让我意识到单纯享受AI带来的生产力红利是不够的我们必须为它建立一套管控体系。这就好比给一辆高性能跑车装上精准的仪表盘、可调节的限速器和完整的行车记录仪。仪表盘让你实时看清“油量”预算消耗限速器防止你一脚油门踩到底导致“超速”超支行车记录仪则完整记录每一次“行程”API调用的细节方便事后审计和优化。“Cursor MCP Proxy Setup Guide”这个项目正是为了解决这个问题而生。MCPModel Context Protocol是Cursor等工具与后端AI模型服务通信的桥梁。而“Proxy”代理则是我们插入这个通信链路中的一个中间层。通过搭建一个自定义的MCP代理服务器我们能够实现两大核心功能预算控制与审计追踪。预算控制意味着你可以为个人、团队或项目设置API调用的月度、甚至单次会话的消费上限一旦接近或超过阈值代理可以自动降级模型比如从GPT-4切换到GPT-3.5-Turbo或直接阻断请求从源头上避免意外的高额账单。审计追踪则是记录每一次MCP请求的详细信息时间戳、用户标识、请求的模型、输入的Prompt、返回的响应、消耗的Token数以及估算成本。这些日志不仅是财务对账的依据更是分析团队使用模式、优化Prompt、提升成本效益比的宝贵数据。本指南将手把手带你搭建这样一个MCP代理。无论你是独立开发者想要管好自己的钱包还是团队技术负责人需要管理多成员的AI工具使用这套方案都能为你提供坚实的管控基础。整个过程不涉及复杂的底层协议修改我们将在应用层巧妙地实现拦截、分析和控制。2. 核心架构与设计思路在通信链路上插入“监控哨所”在开始动手写代码之前我们必须先理解我们要构建的东西在整个系统里处于什么位置以及为什么选择这样的设计。这能帮助我们在遇到问题时更快地定位和解决。2.1 MCP协议与代理的核心角色首先简单理解一下MCP。你可以把它想象成AI编码工具如Cursor和“大脑”如OpenAI的API之间约定好的一种“说话方式”。Cursor用MCP格式的“句子”提出问题然后通过MCP格式接收回答。默认情况下Cursor会直接和OpenAI的服务器“对话”。我们的代理目标就是成为Cursor和OpenAI之间的一个“传话员监督员”。所有从Cursor发出的MCP请求不再直接飞向OpenAI而是先到达我们部署的代理服务器。代理服务器会做三件事检查看看这次请求是否被允许比如是否超预算。记录把这次对话的双方、内容、时间等信息详细记下来。转发把请求原封不动地或稍作修改后转发给真正的OpenAI API并把返回的结果再传回给Cursor。对于Cursor来说它只是把请求发送到了另一个地址我们的代理地址它并不知道中间多了个“监督员”整个体验是无感的。这就是代理模式的美妙之处——对客户端透明。2.2 技术栈选型与理由为了实现这个代理我们需要选择一个技术栈。这里我选择Node.js Express的组合并搭配SQLite数据库。以下是详细的考量Node.js/ExpressMCP通信本质上是HTTP/HTTPS请求。Express是Node.js生态中最成熟、最轻量的Web框架非常适合快速构建RESTful API和中间件。我们将利用Express的中间件机制优雅地实现请求拦截、日志记录和预算检查逻辑。它的异步非阻塞特性也适合处理可能并发的AI请求。SQLite对于预算控制和审计日志这类需求我们需要一个持久化存储。SQLite是一个零配置、无服务器、单文件的关系型数据库。它完美适配我们这个场景轻量无需安装和运行独立的数据库服务如MySQL或PostgreSQL部署极其简单。便携整个数据库就是一个.db文件备份、迁移都非常方便。足够强大完全支持SQL能轻松处理用户表、预算表、审计日志表之间的关联查询。 对于个人或小团队使用SQLite的性能和容量完全绰绰有余。如果未来规模扩大迁移到其他数据库的路径也很清晰。辅助库axios用于代理转发HTTP请求比原生的http模块更易用。dotenv管理环境变量安全地存储API密钥、数据库路径等敏感配置。winston或pino用于生成结构化的应用日志便于调试和监控代理服务器本身的状态。openai官方Node库可选用于更精确地计算Token和成本但初期我们可以用估算公式。注意选择Node.js和SQLite是基于快速原型和易部署的考虑。如果你更熟悉Python使用FastAPI SQLAlchemy SQLite是完全等效的替代方案架构思路完全一致。2.3 数据模型设计定义我们的“账本”和“日志本”数据库的设计是整个系统的基石。我们需要三张核心表users 表记录用户信息。CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, -- 例如邮箱或Cursor配置中的标识 api_key TEXT NOT NULL, -- 该用户使用的OpenAI API密钥加密存储 monthly_budget DECIMAL(10, 2) DEFAULT 0.00, -- 月度预算美元 current_month_spent DECIMAL(10, 2) DEFAULT 0.00 -- 本月已消费 );api_key字段在实际存储时必须加密例如使用bcrypt或Node.js的crypto模块进行加密后再存入数据库绝对不要明文存储。current_month_spent需要在每月1日重置这可以通过一个简单的定时任务cron job或是在每次消费检查时判断月份是否变更来实现。audit_logs 表记录每一次API调用的审计信息。CREATE TABLE audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, model TEXT NOT NULL, -- 请求的模型如 gpt-4-turbo-preview prompt_tokens INTEGER, completion_tokens INTEGER, total_tokens INTEGER, estimated_cost DECIMAL(10, 4), -- 估算成本 request_payload TEXT, -- 可存储精简后的Prompt注意隐私可哈希处理 response_status INTEGER, -- HTTP状态码 FOREIGN KEY (user_id) REFERENCES users (id) );request_payload字段存储完整的Prompt可能涉及隐私和容量问题。一个折中方案是只存储Prompt的哈希值或前N个字符用于问题追踪或者提供一个开关让用户选择是否详细记录。estimated_cost的计算需要根据OpenAI官方定价和使用的模型、Token数来实时计算。我们可以内置一个价格映射表。budget_alerts 表可选但推荐记录预算预警事件。CREATE TABLE budget_alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, alert_type TEXT, -- warning (如达到80%), exceeded (如达到100%) threshold_percentage INTEGER, triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP, notified BOOLEAN DEFAULT FALSE, -- 是否已发送通知如邮件、Slack FOREIGN KEY (user_id) REFERENCES users (id) );这个数据模型虽然简单但已经涵盖了预算控制和审计追踪的核心需求。我们可以在此基础上轻松扩展出“项目级预算”、“团队聚合视图”等功能。3. 代理服务器核心功能实现从零搭建管控中心有了清晰的设计我们现在开始动手实现代理服务器的核心功能。我将分步骤拆解并提供关键代码片段和配置说明。3.1 基础环境搭建与项目初始化首先确保你的系统已经安装了Node.js建议版本16和npm。# 1. 创建项目目录并初始化 mkdir cursor-mcp-proxy cd cursor-mcp-proxy npm init -y # 2. 安装核心依赖 npm install express axios dotenv sqlite3 npm install --save-dev nodemon # 用于开发热重载 # 3. 安装日志和加密工具库按需 npm install winston bcryptjs接下来创建项目的基本结构cursor-mcp-proxy/ ├── .env # 环境变量配置文件不要提交到Git ├── .gitignore # 忽略 node_modules, .env, *.db 等 ├── package.json ├── server.js # 主入口文件 ├── config/ │ └── index.js # 配置管理 ├── db/ │ ├── init.js # 数据库初始化脚本 │ └── database.db # SQLite数据库文件由init.js生成 ├── middleware/ │ ├── auth.js # 用户认证中间件 │ ├── budgetCheck.js # 预算检查中间件 │ └── audit.js # 审计日志中间件 ├── routes/ │ └── proxy.js # 代理路由 └── utils/ ├── costCalculator.js # 成本计算工具 └── logger.js # 应用日志工具在.env文件中配置基础环境变量# 服务器配置 PROXY_PORT3000 NODE_ENVdevelopment # 数据库配置 DB_PATH./db/database.db # OpenAI 相关代理服务器自身可能需要一个默认API Key用于管理或回退 OPENAI_API_KEYsk-your-default-api-key-here OPENAI_BASE_URLhttps://api.openai.com/v1 # 预算告警阈值百分比 BUDGET_WARNING_THRESHOLD80 BUDGET_BLOCK_THRESHOLD1003.2 数据库初始化与连接在db/init.js中我们编写初始化数据库和表的脚本。使用sqlite3库它提供了Promise风格的API更方便使用。// db/init.js const sqlite3 require(sqlite3).verbose(); const path require(path); const { DB_PATH } require(../config); async function initializeDatabase() { const db new sqlite3.Database(DB_PATH, (err) { if (err) { console.error(Could not connect to database, err); } else { console.log(Connected to SQLite database.); } }); // 使用Promise包装便于顺序执行 const run (sql, params []) new Promise((resolve, reject) { db.run(sql, params, function(err) { if (err) reject(err); else resolve(this); }); }); try { // 创建users表 await run( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, api_key_encrypted TEXT NOT NULL, monthly_budget DECIMAL(10, 2) DEFAULT 50.00, current_month_spent DECIMAL(10, 2) DEFAULT 0.00, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ); console.log(Users table created or already exists.); // 创建audit_logs表 await run( CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, endpoint TEXT, model TEXT NOT NULL, prompt_tokens INTEGER, completion_tokens INTEGER, total_tokens INTEGER, estimated_cost_usd DECIMAL(10, 6), request_body_hash TEXT, response_status INTEGER, FOREIGN KEY (user_id) REFERENCES users (id) ) ); console.log(Audit logs table created or already exists.); // 创建budget_alerts表 await run( CREATE TABLE IF NOT EXISTS budget_alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, alert_type TEXT, threshold_percentage INTEGER, triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP, notified BOOLEAN DEFAULT FALSE, FOREIGN KEY (user_id) REFERENCES users (id) ) ); console.log(Budget alerts table created or already exists.); // 可选插入一个默认测试用户密码/API Key需要加密 // const bcrypt require(bcryptjs); // const encryptedKey await bcrypt.hash(sk-test-key, 10); // await run( // INSERT OR IGNORE INTO users (username, api_key_encrypted) VALUES (?, ?), // [demoexample.com, encryptedKey] // ); } catch (err) { console.error(Error initializing database:, err); } finally { db.close(); } } // 如果直接运行此脚本则执行初始化 if (require.main module) { initializeDatabase(); } module.exports { initializeDatabase };在server.js主文件启动时调用这个初始化函数。3.3 用户认证与请求识别中间件我们的代理需要知道每个请求来自哪个用户。一个简单有效的方式是让用户在Cursor中配置一个“自定义API Key”这个Key实际上是我们代理服务器分配给用户的令牌而不是真正的OpenAI API Key。在middleware/auth.js中// middleware/auth.js const db require(../db); // 假设有一个封装好的db模块 const bcrypt require(bcryptjs); async function authenticateUser(req, res, next) { // 方式1从Authorization Header获取Bearer Token const authHeader req.headers[authorization]; if (!authHeader || !authHeader.startsWith(Bearer )) { return res.status(401).json({ error: Missing or invalid Authorization header }); } const proxyToken authHeader.substring(7); // 去掉Bearer 前缀 // 方式2或者从查询参数中获取不推荐因为URL可能被日志记录 // const proxyToken req.query.api_key; try { // 在实际应用中proxyToken可能是JWT或我们自签的令牌。 // 这里简化处理假设proxyToken就是用户的username或id。 // 更安全的做法是有一张单独的api_tokens表来映射token和user_id。 const user await db.get( SELECT id, username, monthly_budget, current_month_spent FROM users WHERE username ?, [proxyToken] ); if (!user) { return res.status(403).json({ error: Invalid or unauthorized token }); } // 将用户信息挂载到request对象上供后续中间件使用 req.user user; next(); // 认证通过继续下一个中间件预算检查 } catch (err) { console.error(Authentication error:, err); res.status(500).json({ error: Internal server error during authentication }); } } module.exports authenticateUser;实操心得在生产环境中强烈建议使用JWTJSON Web Token或类似机制来管理代理令牌。这样可以在令牌中直接编码用户ID和有限的有效期避免每次请求都查询数据库提升性能。同时务必使用HTTPS来传输令牌防止窃听。3.4 预算检查与成本计算中间件这是预算控制的核心。在middleware/budgetCheck.js中我们需要在转发请求到OpenAI之前检查用户当前消费是否已超预算。这里有一个关键点我们无法在发送请求前精确知道本次调用会消耗多少Token和成本。因此预算检查分为两步事前检查检查用户本月已消费额是否已超过“阻断阈值”如100%。如果超过直接拒绝请求。事后更新在收到OpenAI的响应后从响应头中提取usage字段计算本次调用的成本然后更新用户的current_month_spent字段。同时检查是否触发了“预警阈值”如80%并记录预警事件。// middleware/budgetCheck.js const db require(../db); const { calculateCostFromUsage } require(../utils/costCalculator); const { BUDGET_BLOCK_THRESHOLD, BUDGET_WARNING_THRESHOLD } require(../config); async function checkBudget(req, res, next) { const user req.user; const monthlyBudget user.monthly_budget; const spentSoFar user.current_month_spent; // 第一步事前检查 - 是否已超支 if (monthlyBudget 0 spentSoFar monthlyBudget) { // 记录一次“预算已用尽”的审计日志即使请求被阻断 await db.run( INSERT INTO audit_logs (user_id, model, response_status, estimated_cost_usd) VALUES (?, ?, ?, ?), [user.id, blocked-by-budget, 429, 0.0] ); return res.status(429).json({ // 429 Too Many Requests 是一个合适的HTTP状态码 error: Monthly budget exceeded. Budget: $${monthlyBudget}, Spent: $${spentSoFar.toFixed(2)}. }); } // 第二步计算预算使用率检查是否需要预警在事后中间件中触发更准确 // 我们将预警逻辑放在审计日志中间件之后因为那时我们才知道本次消费金额。 // 这里我们先放行请求。 next(); } // 这是一个“事后”中间件用于更新消费和检查预警 async function updateSpendingAndCheckAlert(req, openAIResponse) { const user req.user; const usage openAIResponse?.data?.usage; const model req.body?.model || openAIResponse?.data?.model; if (!usage || !model) { console.warn(Cannot calculate cost: missing usage or model info.); return; } try { const cost calculateCostFromUsage(usage, model); // 更新用户本月消费 await db.run( UPDATE users SET current_month_spent current_month_spent ? WHERE id ?, [cost, user.id] ); // 获取更新后的消费额用于计算百分比 const updatedUser await db.get(SELECT monthly_budget, current_month_spent FROM users WHERE id ?, [user.id]); const spent updatedUser.current_month_spent; const budget updatedUser.monthly_budget; if (budget 0) { const usagePercentage (spent / budget) * 100; // 检查是否达到或超过预警阈值且尚未记录过本次预警 if (usagePercentage BUDGET_WARNING_THRESHOLD usagePercentage BUDGET_BLOCK_THRESHOLD) { // 可以添加去重逻辑避免同一阈值区间内重复报警 const existingAlert await db.get( SELECT id FROM budget_alerts WHERE user_id ? AND alert_type warning AND threshold_percentage ? AND date(triggered_at) date(now), [user.id, BUDGET_WARNING_THRESHOLD] ); if (!existingAlert) { await db.run( INSERT INTO budget_alerts (user_id, alert_type, threshold_percentage) VALUES (?, ?, ?), [user.id, warning, BUDGET_WARNING_THRESHOLD] ); // TODO: 触发通知发送邮件、Slack消息等 console.log(Budget warning alert triggered for user ${user.id}. Spent ${spent.toFixed(2)} of ${budget} (${usagePercentage.toFixed(1)}%)); } } } // 将成本信息挂载供审计日志中间件使用 req.costInfo { cost, usage, model }; } catch (err) { console.error(Error updating spending or checking alert:, err); } } module.exports { checkBudget, updateSpendingAndCheckAlert };成本计算工具utils/costCalculator.js需要维护一个模型定价表。OpenAI的定价可能会变所以最好将其配置化。// utils/costCalculator.js // OpenAI模型定价示例单位美元/每1K Tokens请以官网最新价格为准 const MODEL_PRICING { // 输入Token价格 gpt-4-turbo-preview: { input: 0.01, output: 0.03 }, gpt-4: { input: 0.03, output: 0.06 }, gpt-3.5-turbo-0125: { input: 0.0005, output: 0.0015 }, // 更多模型... }; function calculateCostFromUsage(usage, model) { const pricing MODEL_PRICING[model]; if (!pricing) { console.warn(Pricing not found for model: ${model}. Using default low estimate.); // 返回一个保守估计或0避免因价格未知而阻断服务 return 0; } const inputTokens usage.prompt_tokens || 0; const outputTokens usage.completion_tokens || 0; const inputCost (inputTokens / 1000) * pricing.input; const outputCost (outputTokens / 1000) * pricing.output; const totalCost inputCost outputCost; return parseFloat(totalCost.toFixed(6)); // 保留足够精度 } module.exports { calculateCostFromUsage, MODEL_PRICING };3.5 审计日志记录中间件在middleware/audit.js中我们需要记录每一次请求的详细信息。这个中间件应该在请求被代理转发且收到响应后执行。// middleware/audit.js const db require(../db); const crypto require(crypto); function hashString(str) { return crypto.createHash(sha256).update(str).digest(hex).substring(0, 32); // 取前32位作为哈希 } async function logAuditTrail(req, res, next) { // 保存原始的res.send方法 const originalSend res.send; const startTime Date.now(); // 覆写res.send以便在响应发送前捕获响应数据 res.send function(body) { // 恢复原始方法避免循环调用 res.send originalSend; // 异步记录日志不阻塞响应 (async () { try { const user req.user; const requestBody req.body; const responseBody typeof body string ? JSON.parse(body) : body; const statusCode res.statusCode; // 从请求中提取模型信息OpenAI兼容端点通常是 /v1/chat/completions const model requestBody?.model || unknown; // 从响应中提取Token使用量 const usage responseBody?.usage || {}; const promptTokens usage.prompt_tokens; const completionTokens usage.completion_tokens; const totalTokens usage.total_tokens; // 计算成本如果预算检查中间件已经计算过则使用它 let estimatedCost 0; if (req.costInfo) { estimatedCost req.costInfo.cost; } else if (model ! unknown totalTokens) { // 备用计算方式 const { calculateCostFromUsage } require(../utils/costCalculator); estimatedCost calculateCostFromUsage(usage, model); } // 对请求的Prompt进行哈希处理平衡审计和隐私 const requestBodyHash requestBody.messages ? hashString(JSON.stringify(requestBody.messages)) : null; await db.run( INSERT INTO audit_logs (user_id, endpoint, model, prompt_tokens, completion_tokens, total_tokens, estimated_cost_usd, request_body_hash, response_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), [ user.id, req.path, model, promptTokens, completionTokens, totalTokens, estimatedCost, requestBodyHash, statusCode ] ); const duration Date.now() - startTime; console.log([Audit] User ${user.id} - ${req.path} - ${model} - ${totalTokens} tokens - $${estimatedCost.toFixed(4)} - ${duration}ms); } catch (logErr) { // 日志记录失败不应影响主流程 console.error(Failed to write audit log:, logErr); } })(); // 调用原始的send方法返回响应 return originalSend.call(this, body); }; next(); } module.exports logAuditTrail;注意事项覆写res.send是一个需要小心处理的技术点。确保在异步日志记录中做好错误处理并且不能影响正常的响应流程。另外记录完整的请求/响应体可能包含敏感数据务必根据你的合规要求进行处理如哈希化、脱敏或选择性记录。3.6 代理转发路由的实现最后我们将所有中间件串联起来在routes/proxy.js中创建核心的代理路由。这个路由需要能够处理Cursor发出的各种MCP请求主要是/v1/chat/completions也可能包括/v1/completions,/v1/embeddings等。// routes/proxy.js const express require(express); const router express.Router(); const axios require(axios); const authenticateUser require(../middleware/auth); const { checkBudget, updateSpendingAndCheckAlert } require(../middleware/budgetCheck); const logAuditTrail require(../middleware/audit); const { OPENAI_BASE_URL } require(../config); // 关键顺序很重要认证 - 预算检查 - 审计日志 - 代理转发 router.use(/v1/*, authenticateUser); router.use(/v1/*, checkBudget); router.use(/v1/*, logAuditTrail); // 审计日志中间件会“拦截”响应 // 通配符路由转发所有到 /v1/ 下的请求到OpenAI router.all(/v1/*, async (req, res) { const targetUrl ${OPENAI_BASE_URL}${req.originalUrl}; // 保持原始路径 // 准备转发请求的配置 const headers { ...req.headers, host: new URL(OPENAI_BASE_URL).host, // 修正Host头 authorization: Bearer ${process.env.OPENAI_API_KEY} // 替换为服务器默认或用户指定的真实API Key }; // 注意这里我们使用了代理服务器的默认API Key。更精细的做法是从req.user中解密出该用户自己的API Key并使用。 // 但这样就需要在代理服务器安全地存储所有用户的密钥。简化版先用一个共享密钥。 const config { method: req.method, url: targetUrl, headers: headers, data: req.body, responseType: stream // 使用流式响应以支持OpenAI的流式输出 }; try { const response await axios(config); // 在发送响应给客户端之前更新消费并检查预警 // 注意对于流式响应usage信息在最后的data: [DONE]消息中这里简化处理非流式请求可以这样用。 // 更完善的方案需要解析流式响应中的最后一个data块。 if (!req.headers[accept]?.includes(text/event-stream)) { // 非流式响应可以直接从响应数据中获取usage const responseData response.data; await updateSpendingAndCheckAlert(req, response); // 审计日志中间件会处理响应记录 } else { // 流式响应处理更复杂需要拦截数据流。这里先简化不更新消费可在流结束时处理。 console.log(Streaming request detected, cost update deferred.); } // 设置响应头并转发数据 res.status(response.status); for (const [key, value] of Object.entries(response.headers)) { res.set(key, value); } response.data.pipe(res); // 管道传输响应流 } catch (error) { console.error(Proxy request failed:, error.message); // 如果是Axios错误即OpenAI返回的错误将错误信息传递回去 if (error.response) { res.status(error.response.status).json(error.response.data); } else { res.status(500).json({ error: Internal proxy server error }); } } }); module.exports router;最后在server.js中组装所有部分// server.js require(dotenv).config(); const express require(express); const { initializeDatabase } require(./db/init); const proxyRouter require(./routes/proxy); const { PROXY_PORT } require(./config); const app express(); // 中间件解析JSON请求体 app.use(express.json()); // 初始化数据库仅在启动时一次 initializeDatabase().then(() { console.log(Database initialization check complete.); }); // 使用代理路由 app.use(/, proxyRouter); // 健康检查端点 app.get(/health, (req, res) { res.json({ status: ok, service: cursor-mcp-proxy }); }); app.listen(PROXY_PORT, () { console.log(Cursor MCP Proxy Server running on http://localhost:${PROXY_PORT}); console.log(Configure Cursor to use: http://localhost:${PROXY_PORT}/v1 as your OpenAI API base URL.); });现在一个具备基础预算控制和审计追踪功能的MCP代理服务器就搭建完成了。运行node server.js或npx nodemon server.js启动服务。4. Cursor客户端配置与实战测试服务器跑起来了接下来需要告诉Cursor使用我们的代理。4.1 配置Cursor使用自定义代理Cursor的配置通常在其设置Settings中寻找“AI”或“API”相关部分。你需要修改两个关键配置API Base URL将其从默认的https://api.openai.com/v1改为你的代理服务器地址例如http://localhost:3000/v1。如果你的代理部署在公网则使用对应的公网地址如https://your-proxy-domain.com/v1。API Key这里填写的不再是你的OpenAI API Key而是你在我们代理服务器users表中注册的username或你实现的Token。代理服务器会用这个标识来查找对应用户的预算和审计信息并使用它背后关联的真实OpenAI API Key存储在代理服务器端去发起请求。重要安全提示在生产环境中务必为你的代理服务器配置HTTPS可以使用Let‘s Encrypt免费证书并设置防火墙规则仅允许可信IP如你的团队办公网络IP访问代理端口以防止代理被滥用。4.2 功能测试与验证配置完成后在Cursor中尝试进行一些代码补全或聊天操作。基础连通性测试触发一次AI请求观察代理服务器的控制台输出。你应该能看到类似[Audit] User 1 - /v1/chat/completions - gpt-4-turbo-preview - 150 tokens - $0.0020 - 1200ms的日志说明请求被成功代理和记录。审计日志验证使用SQLite客户端如sqlite3命令行或DB Browser for SQLite打开database.db文件查询audit_logs表应该能看到刚发生的请求记录包含模型、Token数、估算成本等信息。预算控制测试为测试用户设置一个极低的月度预算如$0.01。在Cursor中尝试进行一次稍复杂的操作。代理服务器应该返回429状态码和“Monthly budget exceeded”的错误信息。检查audit_logs表应该会有一条model字段为blocked-by-budget的记录。成本计算准确性验证手动计算一次简单请求的成本根据OpenAI定价和返回的Token数与audit_logs表中的estimated_cost_usd对比看是否大致相符。4.3 数据查看与基本管理为了方便查看和管理我们可以快速搭建一个极简的管理界面或提供一些API端点。在server.js或新建的管理路由文件中添加以下端点务必添加身份验证如管理密码// 示例添加一个受保护的管理员端点用于查看消费摘要 const ADMIN_TOKEN process.env.ADMIN_TOKEN; // 从环境变量读取管理令牌 app.get(/admin/summary, (req, res) { const authHeader req.headers[admin-authorization]; if (!authHeader || authHeader ! Bearer ${ADMIN_TOKEN}) { return res.status(403).json({ error: Forbidden }); } // 简单的SQL查询返回用户消费排行 db.all( SELECT u.username, u.monthly_budget, u.current_month_spent, COUNT(al.id) as request_count, SUM(al.estimated_cost_usd) as total_cost FROM users u LEFT JOIN audit_logs al ON u.id al.user_id GROUP BY u.id ORDER BY total_cost DESC , (err, rows) { if (err) { res.status(500).json({ error: err.message }); } else { res.json(rows); } }); });访问http://localhost:3000/admin/summary并携带正确的Admin-Authorization: Bearer your-admin-token头就可以获取JSON格式的消费报告。5. 生产环境部署、优化与问题排查将代理服务器部署到生产环境并使其稳定可靠地运行还需要考虑以下几个关键方面。5.1 部署方案选择个人使用可以在自己的开发电脑上长期运行或者部署到一台始终在线的家庭服务器/NAS上。使用pm2等进程管理工具来保证服务崩溃后自动重启。npm install -g pm2 pm2 start server.js --name cursor-proxy pm2 save pm2 startup # 设置开机自启小团队使用推荐部署到云服务器如AWS EC2、DigitalOcean Droplet、腾讯云CVM或容器平台如Docker。使用Docker可以简化环境依赖。# Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . EXPOSE 3000 CMD [node, server.js]然后构建并运行docker build -t cursor-mcp-proxy .和docker run -p 3000:3000 --env-file .env cursor-mcp-proxy。安全性增强使用HTTPS在代理服务器前放置Nginx或Caddy作为反向代理配置SSL证书。IP白名单在Nginx或服务器防火墙中只允许团队办公网络的IP地址访问代理的端口。管理界面隔离将管理端点如/admin/*与代理端点/v1/*隔离开使用不同的端口或路径并施加更严格的访问控制。5.2 性能与可靠性优化数据库连接池当前的SQLite连接是每个请求打开关闭在高并发下可能成为瓶颈。可以使用better-sqlite3库支持连接池或为SQLite配置WALWrite-Ahead Logging模式提升并发读写性能。异步非阻塞日志审计日志写入数据库是I/O操作虽然我们用了异步但如果数据库慢仍可能拖累响应速度。可以考虑引入一个消息队列如Redis list或使用Winston等日志库先写入本地文件再由一个后台worker进程批量入库。响应流式传输优化对于OpenAI的流式响应我们目前的简单pipe是有效的但如果代理服务器需要在中途处理数据如实时计算Token则需要更精细的流式处理逻辑。缓存用户信息用户的预算信息变化不频繁可以将其缓存在内存如使用node-cache中一段时间减少数据库查询。5.3 常见问题排查实录在实际搭建和运行中你可能会遇到以下问题问题1Cursor提示“Invalid API Key”或“Authentication Error”。排查检查代理服务器的authenticateUser中间件日志看是否成功解析了Authorization头。确认Cursor中配置的“API Key”是否与代理服务器users表中的username字段一致。检查代理服务器的认证逻辑确保它没有错误地拒绝了有效令牌。解决在代理服务器端打印出接收到的proxyToken与数据库记录对比。确保没有多余的空格或换行符。问题2请求被成功代理但OpenAI返回“Incorrect API key provided”。排查这说明代理服务器成功收到了请求但用它自己的或用户的API Key转发给OpenAI时失败了。解决检查代理服务器环境变量OPENAI_API_KEY是否设置正确且有效。如果你实现了按用户使用不同API Key检查从数据库解密用户API Key的过程是否正确。在代理转发请求前打印出即将发送给OpenAI的Authorization头注意在日志中隐藏真实密钥确认其格式正确Bearer sk-...。问题3审计日志表中没有记录或记录的信息不全如Token数为NULL。排查检查logAuditTrail中间件是否被正确注册且顺序无误应在checkBudget之后代理转发之前。检查覆写res.send的逻辑确保在异步记录日志时没有因为异常导致进程崩溃。检查OpenAI的响应格式确认usage字段存在于响应体的预期位置。流式响应和非流式响应的结构不同。解决在logAuditTrail中间件中添加更详细的调试日志打印出responseBody的结构。对于流式响应需要特殊处理因为usage通常在流结束后的一个独立消息中。问题4预算控制不生效用户超支后依然可以调用。排查检查checkBudget中间件中的查询逻辑确认current_month_spent和monthly_budget字段的值是正确的。确认updateSpendingAndCheckAlert函数在每次成功请求后都被调用并且成本计算正确。检查数据库事务如果有的话确保“读取-检查-更新”的序列在并发请求下是安全的避免竞态条件。解决在checkBudget和updateSpendingAndCheckAlert函数中添加详细的日志输出用户ID、预算、已消费额和计算出的成本。考虑使用数据库的原子操作如UPDATE ... SET spent spent ?来更新消费额确保并发安全。问题5代理服务器响应变慢尤其是处理流式响应时。排查可能是网络延迟、数据库写入瓶颈或服务器资源不足。解决使用console.time测量各个中间件的处理时间。将审计日志改为异步批量写入或写入文件。确保代理服务器与OpenAI API之间的网络连接良好。如果代理在海外而OpenAI API访问慢可以考虑将代理部署在离OpenAI服务区更近的区域。对于流式响应确保代理只是简单地管道传输数据不要在中间进行复杂的同步处理。搭建这样一个MCP代理最耗时的部分往往不是核心功能的编码而是这些边缘情况的处理和调试。建议在开发过程中为每个关键环节都加上清晰的日志并准备一个简单的测试脚本模拟Cursor发送各种类型的请求聊天、补全、流式、非流式来全面验证代理的健壮性。这个项目麻雀虽小五脏俱全涉及了Web开发、数据库设计、API设计、安全性和系统部署等多个方面。当你看到团队的AI工具使用变得透明、可控并且再也不用为突如其来的高额账单而心惊时你会觉得这些投入都是值得的。它不仅仅是一个成本管控工具更是一个让你深入理解团队如何与AI协作的数据窗口基于这些审计数据你还能进一步做很多有趣的优化和分析。