MathTrail

技術者向け

サービスの仕組み

MathTrail は、ご家庭ですでに使っているチャットと、保護者の Google ドライブとの間に立っています。問題はすべてチャットのモデルが書き、その問題を子どもに届けるかどうかはサービスが判断します。MathTrail は単一のプログラムです。自前のモデルは呼び出さず、リクエストとリクエストの間には何も保持しません。

  • Cloud Run 上の単一の Go バイナリ
  • ステートレス
  • データベースなし
  • 自前の LLM 呼び出しなし
  • MCP 2026-07-28 · MCP Apps

チャットクライアント1 · クライアント

  • Claude
  • ChatGPT
  • ほかの MCP Apps ホスト

チャットのモデルが問題を書き、カードがそれを子どもに見せます。

MCP · HTTPS 経由のツール呼び出し

Google Cloud 上の MathTrail2 · ランタイム

Cloud Run · 単一の Go バイナリ · ステートレス

コア:純粋な計算

  • ルール
  • レーティング
  • チェック
  • ソルバー
  • コンテンツ

そのまわり:OAuth、MCP サーバー、カード、封印、制限。

Drive API v3 · 保護者自身のトークンで

Google ドライブ3 · ストレージ

MathTrail/mathtrail-profile.json

子どもごとにファイルを1つ、保護者自身のドライブに置きます。長く保存されるのは、これだけです。

01

構成図

だれがだれとやりとりするのか。子どもも大人も MathTrail に直接アクセスすることはなく、やりとりの相手はチャットホストです。中央のブロックにあるものはすべて単一のプロセスの一部で、別々のサービスではありません。好きな構成要素を選ぶと、その役割とつながり先が表示されます。

サービスの構成要素

ご家庭のチャット

ブラウザ・パソコン・スマートフォンの Claude · ChatGPT

利用者

チャットホスト

MathTrail

単一の Go バイナリ · Cloud Run · 独自ドメイン

構成要素の間は、ただの関数呼び出し

入口

ツール:プロフィールを読み、計算し、書き戻す

純粋な計算:ネットワークなし

コアのまわり

Google と GitHub

バイナリの外にあるサービスはこれだけ

保護者の Google アカウント

Google Cloud

GitHub

図の構成要素を選ぶと、つながっているものは明るいまま残り、ほかは薄くなります。

ご家庭のチャット

大人

保護者または先生。チャットのアカウントと Google アカウントの両方を持ち、MathTrail を接続し、一度だけログインし、プロフィールを作り、チャットでレッスンを進め、進み具合を見守ります。ログインするのも、文字を入力するのも、この人だけです。

ご家庭のチャット

子ども

小学1〜6年生。大人のチャットの中で、カードに出る問題を解きます。自分用のログインはありません。

チャットホスト

ホストのモデル

Claude または ChatGPT。問題、Starlark のソルバー、間違った選択肢ごとの解説、セルフチェックを書きます。カードが表示されない場面では、言葉でレッスンを進めます。MathTrail 自体はモデルを呼び出しません。

チャットホスト

MCP Apps ランタイム

サンドボックス化されたフレームにカードを描画し、言語・プラットフォーム・サイズを渡し、カード自身の呼び出しをホストのトークンをつけて転送します。カード自体がトークンを目にすることはありません。

チャットホスト

クライアントメタデータ文書

ホストは HTTPS のアドレスで自分を名乗ります。MathTrail はクライアントの登録簿を持たず、SSRF 対策をしたうえで、そのアドレスにある文書を取得します。

MathTrail · 入口

HTTP ルーター

唯一の入口:/health、/mcp、OAuth のエンドポイント、.well-known。タイムアウト、ボディサイズの上限、Origin のチェックを備えます。発行者は自身のドメインで、それ以外の Host には404を返します。

MathTrail · 入口

認可サーバー

MathTrail は、PKCE を備えた自前の OAuth 2.1 サーバーです。メタデータ、独自の同意画面、Google でのログイン、トークン、更新と取り消しを扱います。状態を持つサーバーなら保持するはずのものは、すべてトークンと Cookie に封印されています。

MathTrail · 入口

MCP サーバー

プロトコル:Streamable HTTP 2026-07-28、ステートレス、公式の Go SDK を使用。ツールの一覧と呼び出しを扱い、カードを ui:// リソースとして提供し、モデル向けの指示をそのバージョンとともに渡します。

MathTrail · 入口

制限

ログイン前はアドレスごと、ログイン後はアカウントごとのペースと、全体の上限。ここまではすべてメモリ上です。その先に、プロフィールが数える一日の上限があります。制限を超えると、モデルがそのまま伝えられる平易な一文を返します。

MathTrail · ツール

ツール

12個のツールは、どれも同じ形です。プロフィールを読み、計算し、書き戻す。ほとんどはモデルが呼び出し、カードは自分用のもの(問題、答え、記録、プロフィールのフォーム用)を呼び出します。結果に秘密の情報が入ることは決してありません。

MathTrail · 純粋な計算

ルール

指示書を決定的に選びます。間違えたあとは、定着させるために同じテーマ。そうでなければ、いちばん長く出ていないテーマ。難易度は、正解する確率が70〜85%になる目標帯から選びます。子どもが遅れているテーマは2倍の頻度で出ます。今はまだ難しすぎるテーマは一時停止中になり、子どもの全体のレベルがそのテーマに届くと、6問に1回、テーマの確認として戻ってくることがあります。

MathTrail · 純粋な計算

レーティング

P = 0.2 + 0.8·σ(θ + δ − β)。Elo は答えが正しいかどうかだけで動き、その幅はしだいに小さくなります。チェスと同じ尺度で、ランクは11段階。テーマのマスターは、慎重な推定で判定します。

MathTrail · 純粋な計算

チェック

提出された問題が通らなければならないもの:構造と5つの異なる選択肢、レッスンの言語の文字、互いに異なる解説、ソルバー、セルフチェック、読みやすさ、ほぼ重複、そして図(形式、問題文と照らし合わせた記号、図に答えが書かれていないこと)。不合格の点は、すべて一度に伝えます。

MathTrail · 純粋な計算

ソルバー

モデルが書いた Starlark のプログラムを実行し、選択肢を試します。正解になる選択肢は、ちょうど1つでなければなりません。ステップ数と時間に上限があり、ネットワークもファイルも使えません。

MathTrail · 純粋な計算

コンテンツ

バイナリに組みこまれた、コード以外のものすべて:テーマ・ひっかけ・スキルのカタログ、お手本問題、スキーマ、ソルバーのテンプレート、図の種類ごとの例、モデル向けの指示とそのバージョン。

MathTrail · コアのまわり

ウィジェット

カード:Preact を単一の HTML ファイルにビルドしたもので、外部からは何も読みこまず、各言語の辞書も中に含みます。レイアウトは幅320pxから使え、ボタンは指で押しやすい大きさです。

MathTrail · コアのまわり

ストア

隠しマークでプロフィールを見つけ、書きこむ前に比較し、2つのタブの食い違いを調整し、保護者に頼まれればドライブで固定した版からファイルを復元します。そのうえで、ドライブの呼び出しを少ない予算内に収めます。

MathTrail · コアのまわり

封印

1つのしくみで2つの役目を果たします。ログインと Google のトークンを、ホストが持つトークンの中に封印し、問題の答え(現在の問題のものと、先に書かれた問題のもの)をプロフィールの中に封印します。鍵は Secret Manager から取得し、鍵 ID で区別してローテーションします。

MathTrail · コアのまわり

ログ記録

呼び出しごとに、構造化された要約を1件:ツール、結果、所要時間、指示のバージョン。試行と却下の理由は、問題のイベントとして記録します。個人データ、問題文、答えは記録しません。

MathTrail · コアのまわり

コンテナと設定

構成要素をつなぎ合わせて逆の順序で閉じ、タイムアウトとグレースフルシャットダウンを備え、設定はすべて環境変数から読みこみます。開発用のスイッチが入ったままでは、Cloud Run 上で起動しません。

保護者の Google アカウント

Google OAuth

drive.file スコープだけを使った保護者のログイン、その更新と取り消し。どのトークンを取り消しても、すべてのアクセスが終わります。

保護者の Google アカウント

Drive API v3

唯一の長期ストレージ:保護者のドライブで目に見える MathTrail フォルダにある、単一の JSON プロフィール。保護者自身のトークンで読み書きします。

Google Cloud

Secret Manager

封印の鍵と Google のクライアントシークレット。固定したバージョンを、インスタンスの起動時に一度だけ読みこみます。

Google Cloud

Artifact Registry

各リビジョンのイメージを、ダイジェストで指定して保管します。古いイメージは削除されます。

Google Cloud

Cloud Logging

サービスの stdout の行き先です。子どもを数えるための行は、専用のバケットに62日間保存されます。

Google Cloud

BigQuery → Data Studio

利用の集計は何年も保存され、毎晩数え直されます。子どもを数えるときに使う名前は、どのテーブルにもありません。非公開のレポートと公開のレポートがあり、公開のほうは子どもが10人未満のグループを表示しません。

GitHub

Actions

ビルド、チェック、イメージの公開、そして Workload Identity 連携によるデプロイ。サービスアカウントキーは使いません。

GitHub

Pages

このサイト:テーマのページと、同意画面からリンクされるプライバシーポリシーと利用規約。

02

呼び出しごとに見るレッスン

モデルとカードは同じツールを呼び出し、信頼できる唯一の情報源、つまり保護者のドライブにあるプロフィールのファイルを土台にしています。レッスンを続けるために、両者が互いに何かを伝え合う必要はありません。

シナリオ
  • 呼び出し
  • その応答
  • ホスト内部

問題

  • 大人
  • カード
  • ホストのモデル
  • MathTrail
  • ドライブ
  1. 1大人 → ホストのモデル: 「新しい問題をお願いします」

  2. 2ホストのモデル → MathTrail: next_task

  3. 呼び出しが始まるとすぐ、ホストがカードを表示:「次の問題を用意しています…」

  4. 制限 · ルールがテーマ、レベル、難易度を選ぶ

  5. 3MathTrail → ドライブ: 進行中のリクエストを書きこむ

  6. 4MathTrail → ホストのモデル: リクエストは進行中:パッケージを取得する

  7. 5ホストのモデル → MathTrail: get_package

  8. 6MathTrail → ホストのモデル: 指示書 · お手本問題3問 · ひっかけ · テンプレート · 図の例

  9. モデルが問題、Starlark のソルバー、セルフチェックを書く

  10. 7ホストのモデル → MathTrail: submit_task(最大3回)

  11. チェック · ソルバーが正解の選択肢をちょうど1つ見つけること

  12. 8MathTrail → ドライブ: 問題が現在の問題になり、答えは封印される

  13. 9カード → MathTrail: read_task(カードが4秒ごとに問い合わせる)

  14. 10MathTrail → カード: 問題文 · 図 · 選択肢5つ · ヒント(答えはなし)

どの呼び出しもドライブからプロフィールを読みますが、その読みこみは省いています。待機中のカードがパッケージを見ることはありません。パッケージのお手本問題には答えがついているからです。子どもが解いている間に、モデルは prepare_task で次の問題を先に書いておきます。次の問題を求められると、next_task がそれをすぐ新しいカードに出すので、この待ち時間はありません。

答え

  • 大人
  • 子ども
  • カード
  • ホストのモデル
  • MathTrail
  • ドライブ
  1. 1子ども → カード: Cを押す

  2. 2カード → MathTrail: submit_answer(チャットのターンは使わない)

  3. 最初の答えだけ · ここで封印を開ける · θとδは正誤だけで動く

  4. 3MathTrail → ドライブ: レーティングと履歴を書きこむ

  5. 4MathTrail → カード: 正誤 · Cの裏のひっかけ · 解き方の図 · 手順 · レーティングの前 → 後

  6. カードが、問題のあった場所で「こたえのふりかえり」に変わる

  7. 5カード → ホストのモデル: ui/update-model-context(1行。大人の次のメッセージと一緒に読まれる)

  8. 6大人 → ホストのモデル: 「どうして柱が1本多いのですか?」

  9. 7ホストのモデル → 大人: まさにこの問題について答え、カードの内容を補う

ボタンを押すと、何かが説明される前に答えが記録され、カードはすぐに「こたえのふりかえり」に変わります。チャットのターンは使われず、モデルは何も描画しません。モデルはカードからの1行を大人の次のメッセージと一緒に読み、カードの内容をくり返さずに、問題についての質問に答えます。ホストがモデルに送るこの1行が失われても、モデルは次の呼び出しで追いつきます。

ログイン

  • 保護者のブラウザ
  • ホスト
  • MathTrail
  • Google
  • ドライブ
  1. 1ホスト → MathTrail: POST /mcp(トークンなし)

  2. 2MathTrail → ホスト: 401 · メタデータの場所

  3. 3ホスト → 保護者のブラウザ: /oauth/authorize を開く · PKCE S256

  4. 4保護者のブラウザ → MathTrail: authorize

  5. 独自の同意画面:どのクライアントか、保護者をどこへ戻すか

  6. 5保護者のブラウザ → Google: Google でログイン · drive.file のみ

  7. 6Google → 保護者のブラウザ: Google のコードを持って戻る

  8. 7保護者のブラウザ → MathTrail: /oauth/callback

  9. 8MathTrail → Google: コードを交換

  10. 9MathTrail → ドライブ: /about(だれのドライブか?)

  11. ユーザー ID = ドライブのアカウントの HMAC · Google のトークンを自前の認可コードに封印

  12. 10MathTrail → 保護者のブラウザ: 302 · code = mt1.c.…

  13. 11保護者のブラウザ → ホスト: コードを返す

  14. 12ホスト → MathTrail: /oauth/token · コード + コード検証子

  15. 13MathTrail → ホスト: アクセストークン15分 · 更新トークン30日

保護者のログインは一度だけです。サービスはこのやりとりを何も保持しません。リクエストは封印された state に、Google のトークンは封印されたコードに入って運ばれます。

ステートマシンとして見た現在の問題

「一度だけ」のルールはどれもプロフィールのフィールドで、同じ読みこみ・計算・書きこみの中でチェックされます。ほかに何かを覚えているものはありません。

nonenext_task指示書の準備完了requestedsubmit_task ✓封印して出題issuedsubmit_answer一度だけ記録answered

  • requested ↺却下、まだ試行できる(リクエストごとに3回まで)
  • requested → none却下3回、またはリクエストの開始から15分。その日の問題数は減らず、却下3回はその日の失敗したリクエストとして数える
  • issued → requested答える前に「次の問題」:スキップとして記録し、レーティングはそのまま
  • answered ↺その後の答え(別のタブやモデルからのもの)には最初の結果を返し、何も書きこまない
  • answered → requested次の問題は新しいカードに。答えた問題は、もう現在の問題ではない
  • answered → issued次の問題が先に書かれていた:next_task がそれをすぐ新しいカードに出し、requested を飛ばす
  • issued → none封印をもう開けられない(鍵が2回ローテーションした)。何も記録しない

03

状態の置き場所

状態を持つサーバーならデータベースに入れておくものは、すべてほかのだれかが持っています。チャットが保持する封印されたトークン、保護者のブラウザの Cookie、そして保護者自身のドライブです。サービスを再起動しても、2つ目のコピーを起動しても、何も失われません。

MathTrail 自体

なし

アカウントも、セッションも、登録も、Google のトークンもありません。リクエストはそれぞれ、必要なものをすべて自分で運びます。

チャットのトークンストア

封印されたトークン。ホストには中身が見えない

アクセストークン
15分
更新トークン
30日、最長90日

中には、Google のトークン、制限に使う中身の見えないユーザー ID、保護者がログインした国が封印されています。

保護者のブラウザ

2つの Cookie、セッションなし

承認済みのクライアント
180日
ログインの保護(CSRF)
10分

Cookie を失っても、同意画面がもう一度出るか、ログインし直すだけです。

保護者のドライブ

長く保存されるのはここだけ

mathtrail-profile.json
削除されるまで

プロフィール、レーティング、現在の問題(答えは封印済み)。保護者はファイルを開くことができ、ドライブには版の履歴が残ります。

Secret Manager

インスタンスの起動時に一度だけ読みこむ

封印の鍵ローテーション後も、前の鍵は引き続き受け付ける
90日
Google のクライアントシークレット
固定

インスタンスのメモリ

キャッシュのみ:再起動で消えても、何も壊れない

リクエスト頻度のカウンター
数分
プロフィールのファイルの場所
10分
クライアントの文書
5分〜24時間

v1 であえて入れなかったもの

  • データベース
  • キュー
  • セッションストア
  • 共有キャッシュ
  • 作り置きの問題バンク
  • LLM API クライアント
  • REST と OpenAPI

04

プロフィールのファイル

子ども1人に JSON ファイル1つ。サーバー上にある何かの真実のキャッシュではなく、真実そのものです。どのブロックにも上限があり、書きこむたびに守られるので、ファイルが際限なく大きくなることはありません。ふつうのファイルは目標の範囲に収まり、上限に近いファイルは目標を超えます。過去の問題は、本文ではなくフィンガープリントとして残ります。

ファイルのサイズ

約61KB / 約79KB

表示するサイズ
ブロック中身書きこみ元サイズ
フィンガープリント出題した問題のスケッチを最大200件。ほぼ重複のチェック用で、本文・テーマ・日付は含まないnext_tasksubmit_task27KB / 29KB
レーティングの履歴最近の答えそれぞれの直前のθとテーマのδ、そして答えのあった直近7日の各日の始まりのθと全テーマsubmit_answer14KB / 15KB
レーティングとテーマθと子どもの出発点。テーマごと:δ、答え、かかったひっかけ、マスターとそのレベルnext_tasksubmit_tasksubmit_answer8KB / 14KB
最近の答え直近20件の答えとスキップ。ただし、ルールが読む5件の答えを下回ることはないnext_tasksubmit_answer4KB / 5KB
現在の問題(封印)答え、間違った選択肢それぞれの裏にあるひっかけ、解き方、ソルバー。これらをまとめた、中身の見えない単一の文字列next_tasksubmit_tasksubmit_answer4KB / 8KB
現在の問題(平文)カードに表示されるもの:問題文、図、5つの選択肢、ヒント。答えたあとは、答えそのものもnext_tasksubmit_tasksubmit_answer2KB / 4KB
進行中のリクエスト問題を書いている間:その ID、指示書、使った試行回数、テーマを選んだのはだれかnext_taskprepare_tasksubmit_task1.5KB / 2KB
サービスと子どもスキーマのバージョン、ランダムな UUID、リビジョン。ニックネーム、学年、好きなこと、メモ。その日のカウンターsave_profileedit_profile書きこみのたび0.6KB / 1.5KB

先に書かれた問題は現在の問題の隣に置かれ、ほぼ同じ大きさです。ファイルに含まれないもの:過去の問題の本文、子どもが答えるまでの平文の答え、保護者がだれかわかるもの、そしてランク(ランクは表示するときに計算します)。ドライブには条件付きの書きこみがないため、保存の前にファイルをもう一度読み、変わっていれば書きこみを拒否します。2つのタブが黙って互いを上書きすることはありません。

05

封印されるものと、その期間

1つのしくみが2つの役目を果たします。ログインはチャットが持つトークンの中に、問題の答え(現在の問題のものと、先に書かれた問題のもの)はプロフィールの中に封印します。封印にはそれぞれ用途が書きこまれているので、アクセストークンが更新トークンになりすますことはできず、封印された答えがほかの何かになりすますこともできません。

トークンの分解

mt1.a.kQ7fWx.5rJ0Yb2mQ8vK1nT4pXc9…

mt1
形式のバージョン
用途
c コード · a アクセス · r 更新 · d クライアント · s state · k 同意 · t 問題の答え
鍵 ID
鍵そのものから導くため、ローテーションしても気づかれない
暗号文
XChaCha20-Poly1305、192ビットのランダムなノンス

JWT ではありません。サービス以外にこれを読むものはなく、中身の見えない文字列からクレームがログに漏れることもありません。

有効期間(対数目盛り)

  • 認可コード
    60秒
  • ログイン処理中
    10分
  • アクセストークン
    15分
  • 更新トークン
    30日(スライド式、最長90日)
  • 封印の鍵
    90日ごとにローテーション
  • 承認済みのクライアント
    180日

状態を持たないサーバーはコードを使用済みにできないため、コードの有効時間は短くしています。ローテーション後も前の鍵を受け付けるので、鍵が変わってもトークンや封印された答えが失われることはありません。