mcpField
T
tectonic plates

tectonic plates

远程服务
tectonicplates.app健康医疗11 天前更新

关于 tectonic plates

Tectonic plates 是面向杠铃力量训练者的训练记录与计划应用,可将计划展开为包含热身递增组的训练记录,并标明计划值与实际完成值。其经过 OAuth 2.1 身份验证的 MCP 端点可供 LLM 访问每个账户的数据。

主要功能

  1. 01记录训练、动作、组次与重量
  2. 02将计划生成训练周和训练组
  3. 03计算热身递增组与每侧杠铃片
  4. 04区分计划值和实际完成值
  5. 05按账户提供经审计的 MCP 工具

适用场景

  1. 01预先规划多周杠铃训练
  2. 02训练时逐组查看并完成计划
  3. 03记录偏离计划的重量或次数
  4. 04通过 MCP 连接器读取个人训练数据

MCP 相关

`/mcp` 端点通过 OAuth 2.1 身份验证,向 LLM 暴露按账户隔离并经过审计的工具。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://tectonicplates.app/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;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_ENABLEDon全局紧急开关;值为 false 时,所有写入工具都会拒绝操作,而读取功能仍可使用。
MCP_ALLOWED_HOSTS—环回地址之外额外允许的 Host 值(以逗号或空格分隔)。
MCP_ALLOWED_ORIGINS—额外允许的浏览器 Origin 值。
MCP_AUDIT_READSoff同时审计读取工具(写入操作始终会被审计)。
MCP_SCOPESread write服务识别的作用域。
MCP_PUBLIC_BASE_URL—公共 https 源,用于令牌受众和发现 URL。生产环境必需。
OAUTH_JWT_PRIVATE_KEY—用于签署访问令牌的 RSA 私钥(PEM);公钥部分会自动派生。生产环境必需;否则会生成临时密钥对。
OAUTH_REDIRECT_URI_ALLOWLISTClaude、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。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。