2026 年 10 月 3 日更新:SprintFlint 现在在 Railway 上运行。单独的 Web 和 MCP 服务仍然共享一个 Rails 代码库。阅读 我们如何移动服务、数据、上传和备份。下面的文章描述了我们早期的 Heroku 设置。
当我们重新启动公共 MCP 服务器时 mcp.sprintflint.com,来自工程方面的观众自然会提出的问题是: “这是一个单独的代码库吗?” 事实并非如此。 SprintFlint 是一个 Rails 8 应用程序,在 Heroku 上部署了两次;一次作为面向用户的 Web 应用程序,一次作为 MCP 服务器;两者具有相同的代码。这篇文章是工程诚实的版本,说明了我们为什么选择这种形状、多进程文件构建包实际上为您做了什么,以及我们在生产中运行它的前两周观察到的故障模式。
决策树
当您想要将 MCP 服务器与 Rails SaaS 一起交付时,三种可能的形状:
- 一个 Rails 应用程序,一个 dyno。 在主应用程序的路由中安装 MCP 服务器。从代码组织的角度来看,爆炸半径最小,但 MCP 代码中的每次内存泄漏都会导致面向用户的应用程序崩溃,并且 MCP 流量的每次峰值都会消耗 Web 请求容量。
- 一个 Rails 应用程序,通过多进程文件分离测功机。 相同的代码库,不同的进程。 MCP 流量和 Web 流量落在不同域后面的不同测功机上,但它们共享模型、身份验证、迁移和部署。
- 单独的 Rails 应用程序,单独的存储库。 MCP服务器是它自己的服务。最大程度的隔离。最大维护费用;您现在遇到了重复的身份验证代码、模型和迁移协调问题。
大多数 SaaS 初创公司应该选择 (2)。我们做到了。原因如下。
为什么“一个回购,两个动态”是正确的默认设置
当 MCP 服务器位于 SaaS 旁边时,三个属性很重要:
架构是共享的。 MCP 工具列出工单、项目、冲刺。这些与网络应用程序使用的模型相同。在两个存储库中复制它们是两全其美的最糟糕的事情;在工具返回错误数据之前,您不会注意到模式漂移。
身份验证已共享。 相同的不记名令牌适用于 REST 和 MCP。实施两次令牌验证意味着在加强安全性时需要修复两个地方。另外:账户范围、组织成员资格、速率限制分类账;全部单一来源。
部署是协调的。 当您发布模型更改时,Web 应用程序和 MCP 服务器都需要在同一部署窗口中进行更改。拆分存储库设计迫使您考虑跨两个部署的迁移顺序;多进程文件设计只运行一次迁移,两个测功机都会在下次启动时拾取它。
爆炸半径的说法则相反:如果您从与 Web 相同的 dyno 提供 MCP,则 MCP 特定代码中的错误可能会导致 SaaS 瘫痪。一个代码库中的两个动态是一种折衷方案,它可以在运行时为您提供隔离,而无需重复两个存储库。
多进程文件构建包的实际工作原理
Heroku的 多进程文件构建包 是十二行 bash。它读取环境变量(PROCFILE) 在 slug 编译时,复制您指向的文件 Procfile,并让标准 Ruby 构建包接管。
对于我们的设置来说,这意味着:
-
Procfile(默认值;由 sprintflint-product 使用):web: bundle exec puma -C config/puma.rb worker: bundle exec sidekiq release: bundle exec rails db:migrate -
Procfile.mcp(由 sprintflint-mcp 通过使用PROCFILE=Procfile.mcp):web: bundle exec puma -C config/puma.mcp.rb mcp.ru release: bundle exec rails db:migrate
这两个应用程序都是通过相同的 git Push 构建的。区别在于每个人看到的是哪个 Procfile。 mcp.ru 仅安装 ActionMCP 引擎(无设计、无自动播放、无管理),因此我们获得更小的启动占用空间和不同的路由表面,而无需任何代码重复。
我们第一次尝试时犯了什么错误
三件事,按花费我们时间的顺序排列:
1.将MCP路由放在main中 routes.rb. 我们在第一次部署时就这样做了,因为这是阻力最小的路径。然后意识到 MCP 服务器启动到一个配置,其中包括 Sidekiq Web、设备和整个管理 UI,并且公共 MCP 服务器的路由表面应该恰好是一个挂载点。搬到了一个单独的地方 mcp.ru 仅安装 ActionMCP 引擎的rackup 文件。
2. 忘记这一点 release: 运行两次。 两个 dyno 都编译相同的 slug,因此发布短语在每次部署时运行两次。对于 Rails 来说这基本上没问题; db:migrate 是幂等的;但它会使部署时间加倍。我们本可以删除发布阶段 Procfile.mcp 完全(主应用程序的版本涵盖了迁移),但对称性有助于回滚安全:如果另一个 dyno 损坏,则任何一个 dyno 都可以独立启动。
3. 相同 RAILS_LOG_LEVEL 对于两者。 最初我们有 info 无处不在。 MCP dyno 生成的请求量是 Web dyno 的约 10 倍(小型 JSON-RPC 调用,由始终在线的代理轮询),并且通过日志保留而被消耗。将 MCP 切换为 warn 对于 HTTP 请求,Sidekiq 日志保持详细。每个应用程序的 Heroku 配置干净利落地做到了这一点。
Auth:一个令牌,两个表面
共享的不记名令牌是一种功能,而不是一种气味。这就是我们对此充满信心的原因。
令牌在用户的帐户设置中生成,范围仅限于其组织,并在组织级别受到速率限制(600 请求/分钟)。 REST API 和 MCP 服务器都根据相同的令牌验证相同的令牌 ApiToken 模型。不存在“仅 MCP 代币”或“仅 REST 代币”的概念,因为底层功能是相同的;在用户权限内读取和写入组织数据。
反对统一代币表面的论点:
- “如果有人受到威胁怎么办?” 两者都同样受到损害,因为他们读取相同的模型。分裂代币只会使表面积增加一倍。
- “如果我想在不撤销 REST 的情况下撤销 MCP 访问权限怎么办?” 生成两个令牌。撤销一项。代币模型已经支持每个用户多个;我们将它们显示在用户界面中。
- “那么瞄准镜呢?” 当表面具有明显不同的权限形状时,范围是一个真正值得关注的问题。我们的没有。如果我们稍后向 MCP 提供仅限管理的工具,那就是引入范围界定的时候了;不是今天。
监控:两个仪表板,一个日志形状
两个测功机都交付到同一个原木排水管,但带有 dyno=web.N 和 dyno=mcp_web.N 前缀让我们可以在查询中将它们分开。三个顶级仪表板:
- dyno 延迟:web p95 的生存时间为 200-400ms,MCP p95 的生存时间为 50-150ms。它们的形状不同; web做ERB渲染,MCP做JSON-RPC + DB查询。
- 测功机错误率:在部署 Windows 时最有用。如果我们部署 Rails 更改并且只有一个 dyno 的错误率出现峰值,则该错误位于仅其中一个运行的代码中(例如自动播放 UI 或 MCP 引擎)。
-
dyno 达到代币速率限制:告诉我们代理是否在紧密循环中访问 MCP 服务器(有时会这样做;
my-issues每 30 秒轮询一次是一件事)以及稍后是否考虑每个工具的配额。
何时不使用此形状
两个 dyno-one-repo 设计开始出现问题的几种情况:
- 截然不同的缩放配置文件。 如果 MCP 流量增长速度比网络快 100 倍(有可能;代理在每个人工会话中进行许多小调用),最终部署协调将成为一个约束。拆分为单独的代码库后,MCP 团队可以每天交付,而无需对 Web 流程进行回归测试。
- 不同的安全态势。 如果 MCP 需要 PCI 合规性而 Web 不需要,那么单独的 VPC/存储库/审计跟踪开始看起来很有吸引力。
- 不同的语言适合。 Rails 对于今天来说都很好,但如果 MCP 成为高吞吐量流服务,我们可能会用 Go 或 Rust 重写它。这就是这个设计结束的日子。
目前,两个dyno-one-repo 设计正在支付租金。
TL;博士;工程诚实总结
- 一个 Rails 应用程序、两个 Heroku 应用程序、相同的 git Remote
- 多进程文件构建包精选
Procfile或Procfile.mcp基于PROCFILE环境变量 -
mcp.ru仅安装 ActionMCP 引擎;更小的引导,更窄的攻击面 - 共享模型、共享验证、共享部署;不同的运行时,不同的域
- 在“一个应用程序完成所有工作”(无隔离)和“两个存储库”(您会后悔的重复)之间做出妥协
- 当 MCP 的扩展速度比 Web 快 100 倍或需要不同的合规性状态时,将重新访问
如果您要在 Rails SaaS 旁边构建公共 MCP 服务器,则可以从这种形状开始。从这里出发。
相关
- 在你的代理中冲刺状态;从业者指南
- Claude Desktop 用于冲刺管理
- /mcp 登陆页面:完整的服务器文档
- /工具/mcp-config-generator:每个编辑器的粘贴就绪配置