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. 04MCP コネクター経由で個人データを参照

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 エンドポイントは、LLM がコネクターとして利用できるよう同じデータを公開します。これがこのファイルの後半で説明する内容です。

スタックは、Postgres 上の Roda と Sequel で構成されています。アカウントには Rodauth を使用し、rodauth-oauth は MCP エンドポイントが認証に使用する OAuth 2.1 認可サーバーを提供します。ビューは ERB で、CDN 経由の Tailwind と、ページを読み込まずに変更する必要があるセッション画面の部分には htmx を使用しています。Render にデプロイします。

機能

  • セッション画面(/workouts/:id/session) — ジムで使う画面です。ワークアウトの記録である /workouts/:id とは異なります。上部に進捗バーがあり、セットは種目ごとにまとめられ、ウォームアップは薄く表示されます。各重量の下には片側のプレート構成(「片側 1×25 1×10」)が表示され、各セットにはタップで切り替えられる完了ボタンが付いているため、誤ってタップしてももう一度タップすれば元に戻せます。予定とは異なる方法で実施したセットはその場で入力でき、行はライム色ではなくアンバー色に変わります。予定どおりの完了と異なる方法での完了を見分けられるようにするためです。セッション全体に RPE 評価を付けられ、評価を選ぶためのバー速度ガイドもあります。
  • エクササイズ — アカウント間で共有される組み込みのバーベル種目が 54 種類あり、自分で追加することもできます。組み込みの種目は誰でも閲覧できますが、その種目の下に表示されるセットは常に自分のものだけです。
  • ワークアウト — 一覧、記録画面、日程変更、削除。
  • プログラム — 以下で説明するエンジンです。Ruby で作成し、rake から実行します。現時点では編集用の UI はありません。
  • アカウント — Rodauth によるログイン、アカウント作成、ログアウト、「ログイン状態を保持」。
  • MCP — /mcp の OAuth 2.1 認証済みエンドポイントで、アカウントごとに監査されるツールを提供します。

はじめに

.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 時に接続するため、これがないと何も読み込まれません。Rakefile だけが読み込む .env.rb は、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 です。Sequel のクエリログが有効になるのは development だけです。production と staging では Sentry が初期化され、実際の OAuth キーが必要です。
DB_LOGいいえRACK_ENV の値にかかわらず、Sequel のクエリログを標準出力に出力します。デプロイ環境でクエリを追跡するには、空でない任意の値を設定し、終了後に再び設定を解除してください。Sequel はバインドされた値とともにステートメントをログに記録するため、ログにはメールアドレスや全員の重量とレップ数が含まれます。そのため、デフォルトでログが出るのは開発環境だけです。
SENTRY_DSNいいえSentry プロジェクトの DSN です。RACK_ENV が production または staging の場合にのみ読み込まれます。設定されていない、または空の場合、アプリはエラー報告を無効にして起動し、サービスを提供します。その旨は標準エラー出力に一度だけ表示されます。エラー報告が失われても起動を拒否する理由にはならないため、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 です。2 か所で読み込まれます。config/puma.rb では Puma のスレッド数に、lib/tectonic/db.rb では Sequel のコネクション数に使われます。以前はコネクションプールが Sequel のデフォルトである 4、Puma のスレッド数が 5 だったため、この変数を共通にしています。リクエストスレッドがコネクションを取得できずに待機するだけで、問題のないリクエストが Sequel::PoolTimeout になるのを防ぎます。この名前はホスト側の慣例であり、Rails に関するものではありません。
WEB_CONCURRENCYいいえPuma のワーカー数です。設定されていない場合は 0 で、プロセスは 1 つです。これは rackup がすでに実行していた状態であり、CPU の能力を最大限に活用できます。0 より大きい値を指定すると、Puma はフォークするため、アプリはプリロードされ、各ワーカーは起動時に親プロセスのコネクションを共有せず、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 に進んでください。db:migrate に必要なのは、接続できる場所を指す DATABASE_URL だけです。

スキーマは、統合済みのベースラインがひとつだけです。db:migrate は、すでにテーブルがあるデータベースを再構築したりロールバックしたりせず、マイグレーション済みとして記録します。そのため、統合前の既存データベースは、ベースラインでエラーになることなく、それを採用します。

実行方法
bundle exec rackup config.ru        # http://localhost:9292

config.ru は Rack::URLMap の下にふたつのアプリを並べてマウントします。ひとつは /mcp の MCP エンドポイントで、独自のベアラートークン認証を備え、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 を使います。シェル、.env、または Rakefile で何が指定されていたかには左右されません。これは見た目以上に重要です。spec_helper は各テストの後にすべてのテーブルを空にするため、開発用データベースを指定してテストスイートを実行すると、データが追加されるのではなく消去されます。組み込みのエクササイズライブラリだけは残されます。デプロイ済みデータベースには、誰かがサインアップする前からこのライブラリが存在するためです。スキーマに問題があるデータベースは、引き続き rake 'db:reset[name]' で再構築できますが、実行後に削除する行が残ることはなくなりました。

失敗時のエラーだけでは分かりにくい、ふたつの前提条件があります。

  • Postgres が起動し、マイグレーション済みであること。 app.rb は require 時に接続するため、純粋なユニットテストでも 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 は、グロブをシェルで展開する必要がある erb_lint ではなく、rake タスクです。シェルによる展開ではひとつ下のディレクトリまでしか対象にならず、サンドボックス環境によっては完全に拒否されるためです。Rakefile は Ruby でファイル一覧を作成するため、一覧が読みやすく、再帰的で、CI と手元の環境で同じものになります。

プログラムエンジン

プログラムは計画であり、そこから生成されるのがワークアウトとセットです。計画は 4 つのテーブル — programs、program_weeks、program_days、program_lifts — に保持され、1 週間分を生成すると、 is_completed が false の通常の workouts と sets の行になります。予定されたセッションも、 特別な種類のレコードではありません。記載どおりにセットを行うと is_completed が切り替わり、 異なる内容で行うと weight または reps が変更され、依頼された内容の記録として planned_weight と planned_reps が残ります。

  • programs — ブロックです。名前、start_date、および以下の計算で参照する 2 つの設定があります。 preferred_reps(メイン種目を何回のレップ数で表すか)と is_ascending(ワーキングセットの重量をトップ重量まで上げるか、一定にするか)です。
  • program_weeks — ブロック内の週番号と is_deload です。日付は保存せずに算出します。 ブロックの開始日を起点に、前の週から 7 日後に週が始まります。
  • program_days — 曜日です。Ruby の番号では 1 が月曜日で、「スクワット」などのフォーカスを設定します。 日は日付ではなく曜日なので、実際の日付は所属する週によって決まります。
  • program_lifts — その日の中での位置、sets、reps、is_barbell(バーからウォームアップし、 プレートの計算を表示するかどうか)、is_main(レップ数の設定を適用するかどうか)、および 読み取る負荷とともに指定する progression ルールを持つエクササイズです。fixed と linear は top_weight を、percent は percent_of_max を読み取ります。位置は記述された順序であり、 生成される順序、セッションに表示される順序でもあります。

Tectonic::ProgramSeed(lib/tectonic/program_seed.rb)は、編集用の UI ができるまで、 凍結された Ruby ハッシュとして記述されたプログラムです。プログラム、その週、日、リフトを作成し、 アカウントから参照できる既存のムーブメント(アカウント所有またはライブラリのもの)を再利用します。 既存のものがない名前のみを新規作成します。アカウント、名前、ブロックをキーに冪等となるため、 再シードしても既存のプログラムは変更されません。

Tectonic::ProgramGenerator(lib/tectonic/program_generator.rb)は、プログラムの 1 週間分を ワークアウトに展開します。アカウント、プログラムの日、日付をキーに冪等となるため、 週を再生成してもセットが重複することはなく、実際に行った内容が上書きされることもありません。

生成時に使われる計算はアプリの興味深い部分であり、それぞれ個別に仕様が定められています。

  • SetScheme (set_scheme.rb) — ワーキングセットの数、重量、各セットのレップ数を決めます。RPE-8 のパーセンテージ表を使って処方をプログラムが優先するレップ数に変換するため、155 ポンドで 4×5 の処方は、165 ポンドで 4×3 になります。同じ努力度なら、レップ数が少ないほど重量は重くなります。変換は常にレップ数を減らす方向にのみ行われ、表にあるレップ数の間でのみ適用され、is_main としてマークされたリフトに限られます。そのため、8 レップを処方された補助種目は8レップのままです。プログラムが重量を上げていく場合、先行するセットの重量は、各段階でトップセットより3%低くなります。
  • Warmup (warmup.rb) — 空の45ポンドバーベルからワーキング重量の直下まで、段階的に重量を上げていきます。軽いリフトほど必要なウォームアップ段階は少なく、重量が上がるにつれてレップ数は減ります。対象はバーベル種目のみです。自重、バンド使用、マシンのリフトは、ウォームアップの方法が異なるか、そもそも必要ありません。丸めによって2つの段階の重量が同じになる場合は、重複を省きます。同じバーベルを続けて2回持ち上げても、段階的なウォームアップにはなりません。
  • Plates (plates.rb) — 重量を載せたバーベルの片側に使うプレートの内訳を検索します。深さ優先で、重いプレートから試すため、最初に見つかった正確な組み合わせはプレートの枚数も最少です。解をあきらめる前にバックトラックするため、単純な貪欲探索では不可能と判定されるような扱いにくいラックでも重量を組めます。ラックにあるプレートでは本当に実現できない重量の場合は nil を返し、セッション画面では何も表示されません。
  • Progression (progression.rb) — リフトの次の処方を、直近の処方と実際の実施状況から決めます。処方どおりに実施した linear リフトは5ポンド増えます。ワーキングセットを完了できなかった場合、または処方より少ない内容で実施した場合は10ポンド減り、まったくトレーニングしなかった場合は同じ重量を繰り返します。重量を下げる幅が上げる幅の2倍なのは、重量を増やす判断が間違っていた場合は1回のセッションを無駄にしますが、重量を下げるのは実績に基づく対応であり、失敗した重量に戻るとその週を無駄にするためです。トレーニングをしなかったセッションでは、重量は増減しません。1週間の欠席は証拠がないということであり、これを失敗として扱うと、2週間離れただけで、実際に持ち上げられる重量を大幅に下回る負荷になってしまいます。is_deload の週では、ルールによって決まった重量から10分の1を差し引きます。また、その週の結果を次の進行には反映しません。これにより、回復のたびに重量が段階的に下がるのではなく、ブロックの重量増加を再開できます。
  • Rounding (rounding.rb) — 計算した重量はすべて、書き込む前に5の倍数に丸めます。左右に2.5ポンドずつ追加するのが、実際に載せられる最小の重量変化だからです。

現在、エンジンを利用できるのは2つの 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は4週間で、最後の週がディロードです。負荷が記録されるのは第1週のみです。第1週を処方どおりに実施すると、第2週は170ポンドで3レップ、第3週は175ポンドになります。第3週に処方を達成できなかった場合、ディロードの重量は160ポンドではなく150ポンドになります。各週に到達した時点で、その週の内容を生成してください。重量は週の内容を書き込むときに計算され、その後は変更されないため、再生成してもすでに実施したセッションが書き換えられることはありません。

デプロイ

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 は、作成済みの週をウォームアップのランプアップを含む実際のセッションに変換します。プログラムの日に対して冪等なので、2回実行しても変更はありません。

3つの体重計ツールは読み取り専用であり、今後もそのままです。測定値は機器から得られるものであり、コネクターの役割はそれを記録することではなく、正確に報告することだからです。各ツールは測定元と単位ごとに報告し、異なる測定元の値を平均することはありません。体重計の体脂肪率とキャリパーの体脂肪率は、同じ名前が付いていても別の測定値です。また、1年分の測定値は何百行にもなり、そこからは一つのことしか分からないため、各ツールはデフォルトでサンプル数を含む要約を返します。導出した数値には不確実性も示します。単なる数値ではなく、「27.9%、日々の変動は約±1.5%」のように示すため、機器では判別できない差を進歩と誤認することはありません。目標体重や目標ペース、評価を算出することもありません。

エンドポイントは config.ru で /mcp にマウントされたプレーンな Rack アプリであり、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 に接続し、Dynamic Client Registration を介して自身を登録し、ユーザーは同意ページで認可します。アクセストークンは 有効期間の短い RS256 JWT で、MCP エンドポイントがデータベース照会なしでローカルに検証します。検証対象は署名、有効期限、 audience(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> が必要です。トークンがない場合、形式が不正な場合、有効期限切れの場合、audience が一致しない場合、または署名が不正な場合は、リクエストがトランスポートに届く前に 401 で拒否されます。拒否時には、保護対象リソースのメタデータを示す WWW-Authenticate チャレンジが付与されます。
  • DNS リバインディング対策が有効です。ループバックホストは常に許可されます。デプロイしたホストは MCP_ALLOWED_HOSTS に、ブラウザーのオリジンは MCP_ALLOWED_ORIGINS に登録する必要があります。

設定(すべて環境変数から読み取ります):

変数デフォルト意味
MCP_ENDPOINT_PATH/mcpマウントパスです。
MCP_WRITES_ENABLED有効グローバルキルスイッチです。偽と判定される値を設定すると、読み取りは引き続き動作しますが、すべての書き込みツールが拒否されます。
MCP_ALLOWED_HOSTS—ループバック以外に許可する追加の Host 値(カンマまたはスペース区切り)です。
MCP_ALLOWED_ORIGINS—追加で許可するブラウザーの Origin 値です。
MCP_AUDIT_READS無効読み取りツールも監査します(書き込みは常に監査されます)。
MCP_SCOPESread writeサーバーが認識するスコープです。
MCP_PUBLIC_BASE_URL—トークンの audience とディスカバリー URL に使用する、公開用の https オリジンです。本番環境では必須です。
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":{}}}'

ツールの追加

これがセットアップ全体の目的です。認証、アカウントごとのスコープ設定、引数の検証、書き込みキルスイッチ、拒否理由の説明、監査ログはすべて基底クラスに含まれているため、新しいツールで必要なのは、サブクラスを作成し、名前を付け、スキーマとスコープを宣言し、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 を宣言します。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。