关于 tectonic plates
Tectonic plates 是面向杠铃力量训练者的训练记录与计划应用,可将计划展开为包含热身递增组的训练记录,并标明计划值与实际完成值。其经过 OAuth 2.1 身份验证的 MCP 端点可供 LLM 访问每个账户的数据。
主要功能
- 01记录训练、动作、组次与重量
- 02将计划生成训练周和训练组
- 03计算热身递增组与每侧杠铃片
- 04区分计划值和实际完成值
- 05按账户提供经审计的 MCP 工具
适用场景
- 01预先规划多周杠铃训练
- 02训练时逐组查看并完成计划
- 03记录偏离计划的重量或次数
- 04通过 MCP 连接器读取个人训练数据
MCP 相关
`/mcp` 端点通过 OAuth 2.1 身份验证,向 LLM 暴露按账户隔离并经过审计的工具。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://tectonicplates.app/mcpclaude mcp add --transport http app-tectonicplates-tectonic-plates https://tectonicplates.app/mcp{"mcpServers":{"app-tectonicplates-tectonic-plates":{"type":"http","url":"https://tectonicplates.app/mcp"}}}在 Cursor 中打开{"servers":{"app-tectonicplates-tectonic-plates":{"type":"http","url":"https://tectonicplates.app/mcp"}}}在 VS Code 中打开{"mcpServers":{"app-tectonicplates-tectonic-plates":{"type":"http","url":"https://tectonicplates.app/mcp"}}}{"mcpServers":{"app-tectonicplates-tectonic-plates":{"serverUrl":"https://tectonicplates.app/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
Tectonic
Tectonic 是一款杠铃力量训练追踪器。账户用于记录训练;一次训练包含某个动作的多组训练,每组都有重量和次数,并按实际举起的顺序排列。所有重量均以整数磅为单位。
它不只是一个清单:训练计划会在开始训练前写好。计划会展开为普通的训练组记录,包括热身递增组;每个重量都会四舍五入到杠铃实际可加载的数值。这样,训练时只需依次点选清单,而清单已知道接下来要做什么;每条记录都会并列显示计划内容与实际完成情况。本文件的后半部分会介绍一个 MCP 端点,它能将同一份数据作为连接器提供给大模型。
技术栈采用 Roda 和 Sequel,数据库为 Postgres;账户功能由 Rodauth 提供,MCP 端点则通过 rodauth-oauth 提供的 OAuth 2.1 授权服务器进行身份验证。视图使用 ERB,样式通过 CDN 加载 Tailwind;对于训练页面中需要在不重新加载页面的情况下更新的部分,则使用 htmx。应用部署到 Render。
功能
- 训练页面(
/workouts/:id/session)——健身房训练界面,不同于作为训练记录的/workouts/:id。页面顶部显示进度条;按动作分组显示训练组,热身组会淡化显示;每个重量下方显示单侧杠铃片分解(“单侧 1×25 1×10”);每组都有一个完成按钮,点击即可切换状态,误触后再次点击即可撤销。若实际完成的训练组与计划不同,可在行内录入,该行会变为琥珀色,而不是青柠色,以便区分按计划完成和按不同方式完成。整次训练还可评定 RPE,并提供选择评分的杠铃速度指南。 - 动作——内置 54 种杠铃动作,所有账户均可使用,也可以自行添加动作。所有人都能看到内置动作,但其下显示的训练组始终只属于你自己。
- 训练——列表、记录视图、重新安排时间、删除。
- 计划——下文介绍其引擎。计划用 Ruby 编写并通过 rake 运行,目前还没有用于编辑计划的界面。
- 账户——Rodauth 登录、创建账户、注销、记住我。
- MCP——通过 OAuth 2.1 身份验证的
/mcp端点提供按账户隔离并经过审计的工具。
开始使用
你需要使用 .ruby-version 中指定版本的 Ruby(Gemfile 读取同一文件,因此两者不会出现版本不一致),本地 Postgres,以及——仅运行浏览器测试时需要——Firefox 和 geckodriver。
bundle install
npm install # only for the stylesheet; see below
环境
应用加载时,dotenv 会读取 .env;.env-example 是供复制的文件:运行 cp .env-example .env 即可获得可用的开发环境配置,并连接到 rake db:create 将在下文创建的 tectonic_development 数据库。部署环境中的密钥在该文件中会被注释掉,而不是留空;每个密钥旁都附有生成命令。因为在这里,空值比缺失更糟:dotenv 会将变量设为空字符串,而本项目中的所有回退逻辑都会检查值是否为 nil;空的 DATABASE_URL 会传给 Sequel.connect 并导致错误,而不会回退到默认值。
| 变量 | 必需 | 说明 |
|---|---|---|
DATABASE_URL | 是 | app.rb 会在 require 时连接数据库,因此没有它,任何内容都无法加载。.env.rb 仅由 Rakefile 读取,会根据 RACK_ENV 将其默认设为 postgres:///tectonic_development 或 postgres:///tectonic_test,但它使用 ` |
SESSION_SECRET | 是 | 至少 64 字节。 Roda 的 sessions 插件会拒绝较短的密钥,而 app.rb 会在 require 时构建应用,因此密钥过短或缺失都会导致应用在任何路由被访问前报错。.env-example 中包含一个长度足够的密钥,并在其文本中说明该密钥仅用于开发;部署时请使用 ruby -rsecurerandom -e 'puts SecureRandom.hex(64)' 生成真正的密钥。 |
RACK_ENV | 否 | 未设置时为 development;只有 development 会启用 Sequel 的查询日志;production 和 staging 会初始化 Sentry,并要求提供真实的 OAuth 密钥。 |
DB_LOG | 否 | 无论 RACK_ENV 为何,都会将 Sequel 的查询日志附加到 stdout — 设置为任意非空值即可跟踪部署环境中的查询,用完后再取消设置。Sequel 会记录带有绑定值的语句,因此这些日志行会包含电子邮件地址以及每个人的重量和次数;这就是为什么默认情况下除开发环境外都不记录日志。 |
SENTRY_DSN | 否 | Sentry 项目的 DSN,仅当 RACK_ENV 为 production 或 staging 时读取。未设置或为空时,应用仍会启动并提供服务,但会关闭错误报告,并在 stderr 上提示一次:错误报告缺失并不是拒绝启动的理由,这也是它与 OAUTH_JWT_PRIVATE_KEY 行为不同的原因。 |
RACK_TIMEOUT_SERVICE_TIMEOUT | 否 | 请求可以占用线程的秒数,超过后 rack-timeout 会在该线程中报错;未设置时为 20,设为 0 可关闭 — 本地调试时,只要在调试器中暂停,就应该关闭它。仅由 config.ru 读取,因此测试套件不会受其影响。它适用于 URLMap 的两个分支,包括 MCP;MCP 端点中长期运行的 subscriptions/listen 流不受影响,因为 rack-timeout 会对 app.call 计时,而流式响应体会在该调用返回后写入。 |
RAILS_MAX_THREADS | 否 | 未设置时为 5,会被读取两次:config/puma.rb 将该值设为 Puma 的线程数,lib/tectonic/db.rb 将其设为 Sequel 的连接数。之所以共用一个变量,是因为连接池过去使用 Sequel 默认的 4 个连接,而 Puma 有 5 个线程;第五个请求线程等待连接时,会导致原本没有问题的请求触发 Sequel::PoolTimeout。这个名称沿用宿主环境的惯例,并不表示使用 Rails。 |
WEB_CONCURRENCY | 否 | Puma 工作进程数,未设置时为 0 — 即单进程运行,与 rackup 原本的运行方式相同,并充分利用最多半个 CPU。大于零时,Puma 会 fork,因此应用会预加载,每个工作进程启动时都会断开 Sequel 连接,而不是共享父进程的连接。每个工作进程都会使连接数乘以 RAILS_MAX_THREADS。它还会启用 worker_timeout:Puma 仅在集群模式下执行该设置,因此默认值为 0 时,不会回收响应已返回但请求仍在运行的线程,此时唯一生效的超时设置是 RACK_TIMEOUT_SERVICE_TIMEOUT。 |
MCP 和 OAuth 变量在开发环境中均为可选项,详见下方表格。
数据库
bundle exec rake db:create # createuser tectonic, then the development and test databases
bundle exec rake db:migrate # applies migrate/001_schema.rb against DATABASE_URL
bundle exec rake library:exercises # loads the 54 built-in movements, idempotent on name
db:create 会调用 createuser -U postgres 和 createdb -U postgres -O tectonic,因此假设存在名为 postgres 的超级用户角色,并将数据库交给 tectonic 角色。如果你的 Postgres 中两者都不存在,可以手动执行 createdb tectonic_development,然后直接运行 db:migrate;它只需要 DATABASE_URL 指向一个可连接的位置。
此处的架构是一个合并后的基线。对于已经包含这些表的数据库,db:migrate 会写入基线标记,而不是尝试重建或回滚,因此合并之前就已存在的数据库会采用该基线,而不会因此出错。
运行
bundle exec rackup config.ru # http://localhost:9292
config.ru 使用 Rack::URLMap 并排挂载两个应用:/mcp 处的 MCP 端点有自己的 bearer-token 身份验证,不会接触 Roda 的会话或 CSRF;其他所有路径则由 Roda 应用处理。登录前,/ 会重定向到 /welcome,因此请先前往 /create-account 注册。
样式表
npm run build:css # assets/css/app.css -> assets/css/styles.css
assets/css/styles.css 由 assets/css/app.css 编译生成,并且已提交,因此除了编辑样式表,完全不需要 Node——只要执行 bundle install 并准备好数据库,就足以运行应用和测试套件。
添加此前未使用过的 Tailwind 类后,请重新构建样式表。CI 会重新构建并在文件不一致时失败,因此过期文件会在发布前被发现,而不是发布后才发现:被清除的类不会造成任何其他地方的问题,只会让它原本要设置样式的页面在生产环境中悄无声息地出错。
tailwind.config.js 除了扫描模板,也会扫描 app.rb 和 lib/**/*.rb,因为 button_style、rpe_style、row_style 和日历的 STYLES 表会在 Ruby 中构建类名,而这些类名不会出现在任何模板里。
它以前是 <script src="https://cdn.tailwindcss.com">——也就是会在浏览器中编译的 Play CDN。这意味着关闭 JavaScript 时完全没有 CSS,而且包括同意屏幕在内的每个页面都会加载第三方内容。
运行测试
bundle exec rake test # the whole suite
bundle exec ruby -Ispec spec/set_scheme_spec.rb # one file
这两个变量都不必手动传入。spec_helper 会先设置 RACK_ENV,然后自行指定数据库——运行需要独立数据库时使用 TEST_DATABASE_URL,否则使用 postgres:///tectonic_test——无论 shell、.env 或 Rakefile 之前选择了什么。这比看起来更重要:spec_helper 会在每次测试后清空所有表,因此如果测试套件指向你的开发数据库,它不会为数据库填充数据,而是会清空数据。内置的训练库是唯一会保留的内容,因为部署后的数据库在任何人注册之前就已包含它。rake 'db:reset[name]' 仍然可以重建架构出问题的数据库,但现在测试运行结束后,不会再留下需要清理的数据行。
有两项前置条件,从缺少它们时出现的错误中都看不出来:
- 已迁移的 Postgres。
app.rb会在加载时连接数据库,因此即使是纯单元测试,只要spec_helper加载了应用,就需要一个可用的数据库。 - 真实的 Firefox。
spec/exercises_spec.rb、route_ownership_spec.rb、session_spec.rb、system_spec.rb和workouts_spec.rb会通过 Capybara 和 selenium 在绑定到 9292 端口的 Puma 服务上驱动浏览器。设置MOZ_HEADLESS=1可以避免打开窗口;CI 出于同样的原因设置了该变量。
以下是 CI 会运行的两个代码检查器——必须使用这些确切命令,不能用等效命令替代:
bundle exec rubocop
bundle exec rake lint:views
lint:views 是一个 rake 任务,而不是使用带 glob 的 erb_lint,因为 glob 必须由 shell 展开,而 shell 只会深入一层目录,并且在若干沙盒环境中会直接拒绝执行。Rakefile 会在 Ruby 中构建文件列表,因此列表清晰可读、可递归遍历,并且在 CI 中与你本机使用的列表相同。
程序引擎
训练计划是方案;训练和组则是它生成的内容。方案由四张表保存 ——
programs、program_weeks、program_days、program_lifts ——
生成一周后,它们会变成普通的 workouts 和 sets 记录,其中 is_completed 为 false。计划中的训练并非一种单独的记录:按计划完成一组训练时,只会将 is_completed 设为 true;若训练方式有所不同,则会更改 weight 或 reps,并保留 planned_weight 和 planned_reps,作为原定内容的记录。
programs——训练区块:包含名称、start_date,以及下文计算所需的两个偏好设置:preferred_reps(主训练应采用的次数)和is_ascending(工作组的重量是逐渐增加到最高重量,还是保持不变)。program_weeks——区块中编号的一周,以及is_deload。日期通过计算得出,而不单独存储:从区块开始日期算起,每周比前一周晚七天。program_days——星期几,使用 Ruby 的编号方式,其中 1 表示星期一;以及诸如“Squat”这样的训练重点。训练日对应的是星期几,而非某个具体日期,因此实际日期由它所属的周决定。- **
program_lifts``** ——当天某个位置上的一项训练,包含sets、reps、is_barbell(是否从杠铃开始热身并显示配重计算)、is_main(是否适用次数偏好),以及一条progression规则和它所读取的负重:fixed和linear读取top_weight,percent读取percent_of_max`。位置决定训练的书写顺序、生成顺序,以及它在训练中的显示顺序。
Tectonic::ProgramSeed(lib/tectonic/program_seed.rb)是一个以冻结 Ruby 哈希编写的训练方案,在提供编辑界面之前会一直如此。它会创建方案、周、训练日和训练项,复用该账户已能查看的动作(无论是账户自己的动作还是动作库中的动作);只有找不到匹配名称时才会创建动作。它以账户、名称和区块为依据实现幂等,因此重新播种会原样返回现有方案。
Tectonic::ProgramGenerator(lib/tectonic/program_generator.rb)会将方案中的一周扩展为训练。它以账户、方案训练日和日期为依据实现幂等,因此重新生成一周不会重复创建训练组,也不会覆盖实际完成的训练内容。
它生成训练内容时所用的计算逻辑是应用中最有意思的部分,每一部分都有各自的规格说明:
SetScheme(set_scheme.rb)——每组做多少次、用多大重量、做多少组。它会通过 RPE-8 百分比表,将处方转换为程序偏好的次数,因此 155 磅做 4×5 会变为 165 磅做 4×3:相同的用力程度下,次数越少,重量越大。转换只会减少次数,只会在图表涵盖的次数范围内进行,而且只适用于标记为is_main的举重项目,因此处方为 8 次的辅助动作仍保持 8 次。程序递增时,前面的组会按每一步比最高重量低 3%。Warmup(warmup.rb)——从空的 45 磅杠铃逐步加重,直到略低于工作重量,并分成不同级别;较轻的举重项目所需的递增幅度较小,随着重量增加,次数也会减少。仅适用于杠铃训练:自重、弹力带和器械训练的加重方式不同,或者根本不会递增。如果四舍五入后导致两个递增步骤的重量相同,就会删去重复步骤——同一根杠铃不该举两次。Plates(plates.rb)——给杠铃两侧安排配重片,采用深度优先搜索,并优先使用最重的配重片,因此第一个精确匹配也是配重片数量最少的方案。它会回溯而不是直接放弃,因此即使配重架上的片型不理想,也能找到纯贪心搜索会判定为不可能的重量;如果配重架确实无法配出目标重量,则返回 nil,训练记录视图会将其完全留空。Progression(progression.rb)——根据举重项目上一次的处方以及实际完成情况,计算下一次的处方。按规定完成训练的linear举重项目会增加五磅;工作组未完成或未达到处方要求的项目会减少十磅;完全没练过的项目则保持不变。减重幅度是增重幅度的两倍,因为加重只是一次猜测,猜错会浪费一节训练,而减重是对证据的回应;如果刚刚失败过,再用同样的重量训练,就会浪费一周。整节训练无人完成时,重量不会朝任何方向变化:错过一周只是缺少证据,如果将其视为失败,就会把两周的缺席叠加成远低于训练者实际能力的重量。is_deload周会将规则得出的重量减少十分之一,而且该周本身不会用于计算后续进度,因此训练区块恢复后会继续递增,而不是每次恢复都逐步减重。Rounding(rounding.rb)——每个计算出的重量在写入前都会取整到 5 的倍数,因为每侧加上 2.5 磅是实际可加载的最小变化。
目前,只有两个 rake 任务可以调用该引擎:
bundle exec rake program:seed # ACCOUNT_ID=1, or the only account
bundle exec rake 'program:generate[1]' PROGRAM_ID=1 # week 1; the current week by default
从预置的区块 0 开始运行后,会生成周一的训练安排,其第一项举重训练如下:热身递增为 45×5、95×5、115×3 和 135×2,随后以 150、155、160 和 165 磅各做 3 次——程序要求的 155 磅做 4×5,经过转换和递增后就变成了这样。
区块 0 共四周,最后一周为减量周,且只有第一周会以指定重量生成。若按处方完成第一周训练,第二周会生成 170 磅做 3 次,第三周则为 175 磅;如果第三周未能完成训练,减量周的重量会是 150 磅,而不是 160 磅。每周开始时再生成该周的训练安排:重量会在写入周安排时计算,之后便保持不变,因此重新生成时不会改写已经训练过的内容。
部署
render.yaml 是签入仓库的 Render 蓝图:使用 bundle install 构建,使用 bundle exec rake db:migrate && bundle exec rake library:exercises 作为部署前命令,并使用
bundle exec puma -C config/puma.rb 启动。密钥标记为 sync: false,这样不会更改控制面板中已设置的值——重新生成 SESSION_SECRET 会让所有人退出登录,而重新生成 OAUTH_JWT_PRIVATE_KEY 会导致所有已签发的访问令牌都无法通过验证。
config/puma.rb 设置线程数、工作进程数,以及分叉工作进程所需的钩子;它还会自行绑定 $PORT,因此启动命令中不需要带 -p。Puma 按文件名而不是标志参数查找该文件,所以本地运行 bundle exec rackup config.ru 时会采用相同的线程数,并使用自己的端口。生产环境唯一不同之处在于,可以将 WEB_CONCURRENCY 设为大于零;此时应用会预加载,每个工作进程都会断开继承的 Postgres 连接。
MCP 服务
Tectonic 会暴露一个 Model Context Protocol 端点,让 LLM 客户端通过经过审计的逐账户工具操作账户数据。围绕这些工具的框架——传输、身份验证、范围限定、审计和防护措施——让添加工具变得很简单;请参阅“添加工具”。
| 用途 | 工具 |
|---|---|
| 记录训练 | create_exercise、create_workout、update_workout、create_set、complete_set、update_set、delete_set、rate_workout |
| 查看记录 | list_exercises、list_workouts、get_workout、exercise_history |
| 设定目标 | set_goal、block_progress |
| 体重秤读数 | health_readings、bodyweight_trend、body_composition |
| 编写计划 | list_programs、get_program、create_program、add_program_week、add_program_day、update_program_day、add_program_lift、update_program_lift、delete_program_lift、generate_program_week |
| 连接器契约 | search、fetch(句柄为 exercise:、workout:、program:) |
| 我是谁 | whoami |
程序工具让“为我规划训练”成为可能,而不只是“记录我做了什么”。create_program 会在一次调用中写入一个完整训练周期——周、训练日和动作——因为逐个动作组成训练周期需要几十次往返调用,可能会导致只写入一半。此后的操作都针对单个对象,因为修改计划就是:调整这个动作、移动那一天、添加一个与上一周类似的新周。generate_program_week 会将编写好的训练周转换为实际训练课,并包含热身递增组;它以训练日为幂等单位,因此运行两次也不会造成任何变化。
这三个体重秤工具都是只读的,并且会始终保持只读:测量值来自仪器,连接器的职责是如实报告,而不是记录测量值。每个工具都会按来源和单位分别报告,绝不会跨来源取平均值——体重秤的体脂率和皮脂钳的体脂率是两种不同的测量,只是名称相同——并且默认会返回包含样本量的摘要,因为一年的读数会有数百行,但它们表达的其实是同一件事。派生数值会附带不确定性说明:例如“27.9%,日常波动约为 ±1.5%”,而不是只给出一个数字,这样就不会有人把仪器无法分辨的差异误认为进步。它们不会计算目标体重、目标变化速率,也不会给出评判。
该端点是一个普通的 Rack 应用,挂载在 config.ru 的 /mcp 上,完全独立于 Roda 的会话、CSRF 和静态资源。它使用 mcp gem(固定为 1.2.0),并以无状态模式(stateless: true)构建:每个 POST 请求都自成一体,内存中不会保存会话或 SSE 状态,因此没有单进程限制——它可以像其他请求一样横向扩展。
身份验证(OAuth 2.1)
通过 OAuth 2.1 进行访问,由同一应用中的 Rodauth(rodauth-oauth)提供服务。LLM
客户端(Claude、ChatGPT,……)连接到 MCP URL,通过动态客户端注册自行注册,
然后用户在授权页面上授权。访问令牌是短期有效的 RS256 JWT,MCP 端点会在本地
验证其签名、有效期和受众(RFC 8707),无需查询数据库。无需生成或粘贴任何内容。
根据 MCP 授权规范,发现端点位于根路径:
/.well-known/oauth-protected-resource— RFC 9728;用于指定此资源及其 授权服务器。/.well-known/oauth-authorization-server— RFC 8414;包含授权、令牌、注册 端点、作用域和 S256 PKCE。
对于无头调用方(脚本或 CLI),请注册机密客户端并使用客户端凭据授权:
# registers a client bound to an account; prints client_id + client_secret once
bundle exec rake 'oauth:client:register[My Script]' ACCOUNT_ID=1
# then exchange them at POST /token (grant_type=client_credentials) for a JWT
端点
- URL:
https://<host>/mcp(本地为http://localhost:9292/mcp)。 - 每个请求都需要
Authorization: Bearer <jwt>。缺失、格式错误、过期、受众错误 或签名不正确的令牌都会被拒绝,并返回401— 其中携带的WWW-Authenticate质询会指向受保护资源元数据 — 且请求不会到达传输层。 - 已启用 DNS 重绑定保护。始终允许环回主机;已部署的主机必须列在
MCP_ALLOWED_HOSTS中(浏览器来源则列在MCP_ALLOWED_ORIGINS中)。
配置(全部从环境中读取):
| 变量 | 默认值 | 含义 |
|---|---|---|
MCP_ENDPOINT_PATH | /mcp | 挂载路径。 |
MCP_WRITES_ENABLED | on | 全局紧急开关;值为 false 时,所有写入工具都会拒绝操作,而读取功能仍可使用。 |
MCP_ALLOWED_HOSTS | — | 环回地址之外额外允许的 Host 值(以逗号或空格分隔)。 |
MCP_ALLOWED_ORIGINS | — | 额外允许的浏览器 Origin 值。 |
MCP_AUDIT_READS | off | 同时审计读取工具(写入操作始终会被审计)。 |
MCP_SCOPES | read write | 服务识别的作用域。 |
MCP_PUBLIC_BASE_URL | — | 公共 https 源,用于令牌受众和发现 URL。生产环境必需。 |
OAUTH_JWT_PRIVATE_KEY | — | 用于签署访问令牌的 RSA 私钥(PEM);公钥部分会自动派生。生产环境必需;否则会生成临时密钥对。 |
OAUTH_REDIRECT_URI_ALLOWLIST | Claude、ChatGPT、环回地址 | 客户端可注册的回调地址(以逗号或空格分隔),会替换默认值,而非在其基础上追加。以 / 结尾的条目匹配其下的任意路径;环回地址条目匹配任意端口。 |
将其作为自定义连接器接入
在支持远程 MCP 服务的客户端(Claude、ChatGPT)中,添加自定义连接器:
- 类型:HTTP / “Streamable HTTP” MCP 服务。
- URL:
https://<host>/mcp。 - 身份验证:OAuth — 客户端会从 URL 发现授权服务器,并引导你完成授权页面流程;无需粘贴令牌。ChatGPT 的标准连接器还要求提供
search/fetch工具,服务已提供这些工具。
也可以将任意支持 MCP 的客户端指向该 URL。使用通过 OAuth 流程(或上述客户端凭据授权)获取的 JWT,通过 curl 快速检查:
curl -sX POST https://<host>/mcp \
-H 'authorization: Bearer <jwt>' \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
添加工具
这是整个配置的核心。身份验证、按账户隔离、参数验证、写入熔断开关、拒绝并说明错误,以及审计日志都由基类处理,因此添加一个新工具只需:继承基类、命名、声明 schema、声明作用域、编写 perform、注册。 whoami 是完整示例
(lib/tectonic/mcp/tools/whoami.rb):
class Tectonic < Roda
module MCP
module Tools
class Whoami < Tool
tool_name 'whoami'
description 'Report the authenticated account id, email, and granted scopes.'
scope :read # :read or :write, enforced before perform
input_schema(type: 'object', properties: {}, additionalProperties: false)
def self.perform(context:, **) # context is the resolved account
ok(
"You are account #{context.account_id} (#{context.email}). " \
"Granted scopes: #{context.scopes.join(', ')}.",
structured: { account_id: context.account_id, email: context.email, scopes: context.scopes }
)
end
end
end
end
end
然后在 lib/tectonic/mcp/server.rb 中注册:
TOOLS = [Tools::Whoami, Tools::YourNewTool].freeze
以下功能开箱即用:
context按账户隔离。 它只公开该账户的数据集 —context.workouts、context.exercises、context.sets— 每个数据集都已按经过身份验证的账户筛选。它没有account_id设置器,也没有返回未隔离模型的访问器,因此无法通过 context 表达跨账户查询。工具绝不能接受account_id参数,也不能访问全局模型常量。- 作用域强制执行。 如果令牌缺少工具声明的作用域,工具会在运行
perform之前被拒绝。 - 参数验证。 gem 会根据你的
input_schema验证传入参数,并返回模型可以据此修正的易读错误。 - 拒绝并说明原因。 返回
refuse('...')(或引发Tool::Refusal),并提供供模型读取和处理的消息 — 绝不能返回堆栈跟踪。领域规则(权重合理性范围、拒绝处理已完成的训练、日期幂等性)应通过此路径在工具主体中处理。 - 审计。 每次写入无论成功还是失败,都会在
mcp_audit_log中写入一条记录。只有声明了audit_reads的工具,其读取操作才会被审计。 - 熔断开关。 当
MCP_WRITES_ENABLED关闭时,写入工具会自动拒绝执行。
成功时使用 ok(text, structured: {...}) 返回结果,处理失败时使用 refuse(message)。写入工具声明 scope :write;读取工具声明 scope :read。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。