6502tinymachines

MCP、クライアントとサーバーのために

tinymachines.ai は Model Context Protocol を、公開の URL ひとつで話す:

https://tinymachines.ai/api/mcp

読み取り専用で、アカウントも鍵も要らず、二種類の問いに答える。ツールは tinymachines が何であり、どの部品がいま応答しているかを、尋ねられた時に測って答える。リソースは tm:// という名前空間だ。このサイトの文書、データ、設計ノートをファイルとして、ページを訪問者が受け取るままの姿で、そしてページとその元になった markdown とのあいだに型付きのリンクを張って提供する。名前空間の背後にある仕様はリポジトリの extern/tinypeek/spec/tm-protocol-spec.md(tinypeek サブモジュール)にあり、このページはその実用の側だ。

前半はクライアントのため。後半はサーバーを作る人のためで、私たちのものも含む。

クライアントのために

つなぐ

エンドポイントは Streamable HTTP だ。メッセージごとに POST ひとつ、本文は JSON-RPC 2.0、開くべきセッションも保持すべきイベントストリームも無い。たいていのクライアントは URL だけで足りる。

claude.ai と Claude desktop。 カスタムコネクタを追加して上の URL を渡す。認証は求められないので、空のままでよい。desktop はサインインしているアカウントのコネクタを共有する。

Claude Code。 コマンドひとつで、いまいるプロジェクトに登録される:

claude mcp add --transport http tinymachines https://tinymachines.ai/api/mcp

claude mcp list がそれを表示し、セッション内の /mcp が提供物を表示する。フラグは執筆時点のもので、claude mcp add --help が正典だ。

JSON の設定を取るその他のクライアント。 形はいつものものだ:

{
  "mcpServers": {
    "tinymachines": {
      "url": "https://tinymachines.ai/api/mcp"
    }
  }
}

curl、クライアントが見るものを覗くために。initialize してから list する:

curl -s https://tinymachines.ai/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'

curl -s https://tinymachines.ai/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

initialize の答えには、モデルのために書かれた短い案内である instructions と、ツール、リソース、補完がすべてここにあると言う capabilities が載る。この URL への GET は POST を名指しした拒否を返す。開くべきストリームが無いからだ。

提供されているもの

ツールは四つ:

ツール答えるもの
overview6502 の仕事の六つの部品。それぞれが何で、ソースがどこにあり、ライセンスは何か、そして公開面が上がっているかを、呼ばれた時に測る
pieceキーで指した部品ひとつ、同じ測定つき
licensing何をどの条件で公開してよいか。ひとつのライセンスではない
resolvetm:// の URI をひとつ読み、リンクを本文に載せて返す。下の名前空間へのツール型の入口

そして名前空間が、MCP のリソースとして。文法はホストひとつとマウント七つ:

tm://                                              このサーバーが答えるホストの一覧: ひとつ
tm://tinymachines/                                 マウント一覧
tm://tinymachines/fs/docs/nes/pile.md              サイトのチェックアウトのファイル
tm://tinymachines/fs/docs/nes/                     ディレクトリ。読むと一覧になる
tm://tinymachines/git/nes-bench/docs/pile.md       同じファイルを、取り込み元の
                                                   リポジトリの HEAD で
tm://tinymachines/git/public/docs/words.md?at=v1.0&as=blame
                                                   タグ時点のファイル、行ごとにコミット
tm://tinymachines/http/tinymachines.ai/docs/nes/pile?as=text
                                                   そのファイルがなるページ、テキストとして
tm://tinymachines/offbox/autopsy-models            ビルドの連なりがこの箱を出る所。
                                                   名前はあるが、中身は出さない
tm://tinymachines/entity/pattern/pad-poll          解剖が見つけたパターン。それを持つ
                                                   ゲームとルーチンすべてへのリンクつき
tm://tinymachines/mcp/terrapulse/tools/get_slot    この箱が読む別の MCP サーバーのツール。
                                                   そのサーバーが述べたとおりに
tm://tinymachines/mcp/terrapulse/map               そのサーバーの地図。どのツールがアドレス
                                                   として読めるか、各項目がどれだけ確かか
tm://tinymachines/kinds/pattern                    このホストが配信してきたものの種類の
                                                   ひとつ。メディアタイプ、回数、運んだリンク
tm://tinymachines/http/bradley.io/                 この箱が配信するが組み立ててはいないサイト。
                                                   サイトが公開するサイトマップから並べる
tm://tinymachines/manifest                         視覚言語をデータにしたもの。マウントごとの形、
                                                   リンク関係ごとの色
tm://tinymachines/renderers/                       メディア型ごとに、それを描くモジュール。
                                                   アドレスと sha256 で
tm://tinymachines/entity/pattern/pad-poll?as=blocks  何でも、コードのない画面のための
                                                   素のブロックに組み立てたもの

/ で終わる URI はコレクションで、読むとカーソルつきで子が返る。マウントより後ろはバックエンド自身のパスだ。表現はクエリパラメータであって、パスの接尾辞ではない。?as=stat は同じもののメタデータ、?as=text はページの読める本文(パラメータなしで読んでもこれになる)、?as=rendered は訪問者が受け取るヘッダつきの HTML。

git マウントはサイト自身のリポジトリと、ノートがページを取り込んでくる隣のリポジトリを提供する。すべて GitHub で同じ名前のもとに公開されている。?at= はブランチ、タグ、コミット、あるいは時刻(ISO の日付か日時。その時刻以前に作られた最新のコミットを指す。fs でも使え、entity ではそのときのレコードどおりのエンティティを読む)、?as=log はパスに触れたコミット(&n= で何件か)、?as=blame は行ごとにコミットを添えたファイル、?as=tree はディレクトリを JSON で、&depth= はそれを一度に何段まで読むか(八段まで)。6502 の既定の ref は最新ではなく、サイトが配信しているコミットだ。

ものの一部にもアドレスがある。?lines=FIRST-LAST はファイル、またはページの本文の行(1 から数え、両端を含む)、?pointer=/a/0/b は JSON ファイルの中で JSON ポインタが指すもの、?range=START-END はバイト列を読む。答えの uri はその範囲を保つので、一部を引用したページは出どころを保つ。エンティティの tm:data リンクはこの形でレコードの中を指す。

entity マウントは、ほかの四つの上に最初に建てたものだ。解剖(autopsy)のパターン、ゲーム、ルーチン、レッスンにそれぞれの番地を与える。読むのはサイトがすでに公開している二つの記録で、番地は entity/pattern/<key>、entity/game/<key>、entity/game/<key>/routine/<bank>-<address>、entity/lesson/<key>。どれも JSON として、記録の項目のまま読め、?as=stat はそのメタデータ。番地があるのは規則が名前をつけたルーチンだけで、カートリッジのバイトはどこにも入っていない。レッスンの記録は比べた相手のゲームのキーを持つ。だからレッスンはそのゲームへ結ばれ(tm:measures)、ゲームは戻る(tm:measured-by)。

どの読み取りもリンクを運ぶ。ノートのページは元になった markdown へ(tm:source)、markdown はそのページへ戻る(tm:renders-as)。ビルドが他のリポジトリから取り込んだページとここにある写しは、出所のファイルへ(tm:generated-by)、そのファイルは前向きに(tm:generates)。ここにあるファイルと git の同じファイルは双方向に結ばれる(tm:repository、tm:working-copy)。どのページも、それを描画するルートのファイルへ(tm:source)と、ページ自身のコードが読む記録へ(tm:data)、記録は読み手へ(tm:read-by)と書き手のスクリプトへ(tm:generated-by)。ページは同じページの別の言語版と別のサイト版へ(alternate)、サブモジュールは固定しているコミットのリポジトリへ(tm:pins)結ばれる。トレイル(fs/notes/trails/ の下の JSON ファイルで、名前つきの順序あるアドレスの一覧)はその各段へ(tm:step)、各段はそれが載っているトレイルへ(tm:step-of)結ばれる。パターンは見つかったゲームへ(tm:found-in)、規則が名前をつけたルーチンへ(tm:instance)、それを使うレッスンへ(tm:demonstrated-by)、そしてそれぞれが戻る(tm:exhibits、tm:instance-of、tm:demonstrates)。レッスンはカートリッジのソースへ(tm:cartridge)、ゲームはそのページへ(tm:renders-as)。ルートは最初に読む文書へ(help)結ばれる。そして up、collection、alternate、describedby、version-history、latest-version は IANA のレジストリの言う通りの意味だ。いくつもの出現をまとめて表すリンクは、その数を count に持つ。リンクの並びはひとつに決まっている。IANA のものが先、次に exact、次に inferred、その中では関係の名前、番地の順。ひとつの読み取りが運ぶリンクは百本まで。それを超えるものは最後に ?as=links への tm:links を置く。?as=links は中身なしで全部のリンクをページごとに返し、各ページの終わりに next がある。集合は絞り込める。?as=links&rel=tm:found-in&min=2 は一つの関係だけ、しかも二回以上数えられたリンクだけで、絞り込んだ集合もそれ自身のアドレスになる。リソースを通すとリンクは結果の _meta の tinymachines.ai/links の下に、resolve を通すと本文にある。

mcp マウントは、ほかの MCP サーバーをこのホストのマウントとして読む。mcp/ は箱が読むサーバーの一覧、mcp/<server>/ はひとつのサーバーの記録(何であるか、箱が尋ねたとき答えたか、何を宣言したか)で、その tools/、resources/、prompts/ を並べる。mcp/<server>/tools/<name> はサーバーが述べたとおりのひとつのツールで、そのスキーマはその一部として読める(?pointer=/inputSchema)。ツールは並べられ、述べられる。走るのは、サーバーの地図がそれを名指す所だけだ。mcp/<server>/map は、tinyhelper の索引ドライバーがサーバー自身のスキーマから導き、人が一項目ずつ直した地図で、各項目がどれだけ確かかを言う。地図が名指すパス(たとえば mcp/terrapulse/dex/earthquake/<id>)は、その項目が名指すツールをパスの各節を引数にして呼び、答えがそのものになる。地図が与える種類でタイプづけされ、読むのに使ったツール(tm:read-with)とフィールドが名指すものにリンクし、サーバーの出所(sources、licence_basis)は _meta にそのまま運ばれる。地図のないサーバーは、これまでどおりに読める。箱が持たないトークンを求めるサーバーは gated、答えないサーバーは unreachable と読め、記録がどちらかと、箱が最後に尋ねた時刻を言う。

どの読み取りも、自分が何であるかを mimeType で言う。サーバーが組み立てるものは application/vnd.tm.<kind>+json(listing、stat、record、pattern、tool など)か text/vnd.tm.<kind>(ページの本文は text/vnd.tm.page)。サーバーがただ持っているだけのファイルはそのファイル自身のタイプのままで、markdown は text/markdown のままだ。クライアントがパスから種類を推し量る必要はない。kinds/ はこのホストが記録を始めてから配信してきた種類をすべて並べ、kinds/<kind> はそれが何回、どのメディアタイプで、どのリンク関係を運んで配信されたかを言う。測られたものなので、一度も配信されていない種類はそこにない。リンクは行き先の type を運ぶことがある。entity マウントでは ?as=set&rel=<rel> が、ひとつのリンク関係の先にあるものをまとめて、ページごとに読む。メンバーひとつにつき一回の読み取りが省ける。

この箱が配信するが組み立ててはいないサイトは、サイトが公開する sitemap.xml から並べられ、その robots.txt が柵になる。取りに行くのは並べられたページだけで、サイトマップが名指さないパスは要求が出る前に not-found、Disallow の下のパスはその規則を添えて denied と読める。そのページはページとして読める。サイトが /.well-known/tm-manifest.json にリンクマニフェストを公開していれば、ページの題名はそこから取られ、その stat にはページを作るファイルが並ぶ。マニフェストは並べられたページを説明できるだけで、ページを足すことはできない。

モジュールをまったく持たないホスト、端末や印刷ページのために、ふたつのアドレスがある。tm://tinymachines/manifest は視覚言語をデータにしたもので、マウントごとの形、リンク関係ごとの色の系、種類が持つ取っ手を、ひとつのファイルから読む。だからどの閲覧者も同じものを描く。どのアドレスにも ?as=blocks をつけると、閉じた語彙(heading、note、kv、list、table、timeline、diff、code、media、question、finding、trail、link)のブロックの順序つきの列に組み立てられる。どのブロックも自分が来たアドレスを名指し、本文は組み立てに読んだアドレスをすべて挙げる。だからブロックだけを描くページが、本文を覗かずにどの種類も見せられる。

解決できない URI は、解決できる最寄りのものを添えて拒まれる。だから外れた推測は行き止まりではなく教えになる。読み取りには呼び出し元ごとの上限がある。まとめて使える分があり、その後は一定の速さで戻る。ページはサーバーが取りに行くので、ファイルより多く数える。上限を超えた呼び出しには rate-limited と、待つべき秒数の retryAfter が返り、その秒数を待てば足りる。読んでいるだけなら出会うことはなく、巡回するものは出会う。

サーバーは要求ごとに記録を残す。何が求められたか、いつか、どう終わったか、どれだけ返したか。誰が求めたかは記録しない。アドレスも、その代わりになるものも残さない。記録は長くても三十日で消え、名前空間が配るものの中には入らない。サイズ上限を超えるファイルはサイズと ?range= と ?lines= のテンプレートつきで返る。リソーステンプレートが文法を述べ、補完が生きた値を埋めるので、クライアントは見せられたことのない URI を組み立てて試せる。

最初のセッション

六回のやり取りで、クライアントは冷えた接続からページの背後のソースファイルまで、パスを推測せずに辿り着く:

  1. initialize: 能力と案内。案内は最初に読む文書の名前を挙げる。
  2. resources/templates/list: テンプレート。ホストの一覧にひとつ、ルートにひとつ、マウントごとにひとつか二つ。
  3. tm://tinymachines/http/tinymachines.ai/docs/nes/ を読む: ノートの NES 節、ページごとに。
  4. fs テンプレートの path に docs/nes/pi と打って completion/complete: そう始まるファイル。
  5. tm://tinymachines/http/tinymachines.ai/docs/nes/pile?as=text を読む: ページの本文と、tm:source のリンク。
  6. そのリンクを読む: markdown と、ページへ戻るリンク、それが置かれたディレクトリ、そして取り込み元のベンチのリポジトリのファイル。その ?as=log が履歴だ。

resolve ツールを通せば、同じ六段は uri 引数つきの呼び出しになり、リンクは本文で返る。

もうひとつのエンドポイント

チップ自身はこの URL の背後にはない。6502 のコードを走らせ、アセンブルし、カートリッジを鋳造するのは 6502 API の仕事で、https://6502.tinymachines.ai/api/mcp に自身のエンドポイントと自身のツールを持つ。ここの overview ツールがそれを名指しし、二つは一緒につながれるように設計されている。

サーバーのために

この節は私たちのサーバーがどう作られているかで、あなたのサーバーが同じように話すため、そして私たちのサーバーへの変更が正しい場所に落ちるためにある。コードは api/mcp_server.py(トランスポートとツール)と api/tm.py(名前空間)で、api/test_tm.py がその隣にある。

トランスポートは POST ひとつ

Streamable HTTP に適合するサーバーに必要なものはごく少ない:

  • POST は JSON-RPC 2.0 のメッセージひとつかバッチを受け取り、同じ形で答える。バッチは順序通りのバッチで返る。
  • 通知(notifications/initialized のような、id の無いメッセージ)には、状態 202 と空の本文で答える。
  • GET はクライアントがサーバーからクライアントへのストリームを開く場所だ。サーバー発のメッセージが無いので、私たちのものは 405 と Allow: POST、そしてその理由の一文で答える。
  • initialize は、クライアントの protocolVersion がサーバーの対応するものならそれを返し、そうでなければ話せる最新のものを返す。能力はここで宣言し、他のどこでもしない。
  • プロトコルの誤り(未知のメソッド、不正なパラメータ)は JSON-RPC のエラーだ。拒むツールは isError: true とモデルが読める理由を持つ普通の結果だ。JSON-RPC のエラーはクライアントのためのものであって、モデルのためのものではないからだ。

ここのトランスポートは SDK ではなく手書きだ。短く、依存の無い、間違えようの少ないもので、下で何も古びないことを約束するサービスにはそれが大事だ。サーバーがこれより多くをする(購読、サンプリング、セッション)なら SDK が正しい選択になる。

ツールは記述と実装で、互いに照合される

各ツールは TOOLS の辞書(名前、題、記述、入力スキーマ)と、ハンドラを組む時に渡す表の中の関数だ。ハンドラは組み立て時に、広告された全ツールに実装があり、全実装が広告されていることを assert するので、二つはずれようがない。記述はモデルのために書く。ツールが何に答えるか、最初に何を呼ぶべきか、未知の引数が何を得るか(有効なものを名指しした拒否であって、推測ではない)。

ツールは REST のルートと同じ関数を呼ぶ。自前で答えを計算するツールは、ひとつのふりをした二つ目の面になる。

マウントは三つの操作

名前空間はひとつの文法の背後にあるマウントの集まりで、バックエンドを足すことはマウントを足すことだ。プロトコルもテンプレートもクライアントも変わらない。マウントが実装するのは:

操作入力出力
listコレクションのパスとカーソル子を URI として。それぞれに名前、型、あればサイズ、コレクションかどうか。切れた時は nextCursor
read葉のパスとその表現型つきの内容と、リンク
completeテンプレートの引数と打たれたもの候補の値、上限つき、hasMore フラグつき

そしてどのマウントも同じ統制を受け継ぐ。パスはファイルシステムに触れる前に解決され、マウントのルートの外に落ちれば拒まれる。読み取りはバイト数で上限があり、上限の先は範囲指定で提供する。一覧はページ分けされる。ファイル名のパターンの拒否リストは二枚目の網で、redacted として一覧に載るので、クライアントはファイルがあることを知り、しかし受け取らない。

私たちのものは三つで、それぞれがその形である理由:

  • fs はサイトを配信するチェックアウトの三つのディレクトリ、どのみち GitHub で公開されているものを提供する。ルートは許可リストであって、ホストではない。この箱にはサインインのデータベースと gitignore されたホスティングの手順書があり、名前の拒否リストではどちらも捕まえられないからだ。
  • git は git そのものを、サーバーが教えられたリポジトリの上で読み取り専用に走らせる。引数はすべて引数として渡し、シェルを通さない。ref は使う前にパターンと照合するので、クライアントの名指しがオプションとして読まれることはない。ツリーのパスは正規化し、上へ歩くなら拒む。取り込まれたページがどのリポジトリから来たかは、その事実のただ一つの写しである取り込みスクリプトから読み、二つ目のリストとしては持たない。
  • http はループバックの nginx を通してページを取る。サイトの名前を SNI と Host ヘッダに付けるので、バイト列は訪問者が受け取るものそのものだ。本物の証明書、リダイレクトの表、ポリシーのヘッダ。サイトはこの箱が配信するものに限られ、それがマウントを開放プロキシにしないものだ。?as=text はページの main 要素を取り、ナビゲーションを外す。

リンクは型付きで、双方向で、確度つき

どの読み取りも {rel, href, confidence?, title?} の形のリンクを添える。IANA のレジストリに関係があればそれを使い、確度は付けない。作りからして正確だからだ。残りは tm: の関係で、exact、inferred、low のどれかを言わねばならない。コンストラクタがそれを assert するので、リンクが黙って間違えることはない。逆向きの対は両方とも出すので、辿りはどちら向きにも歩け、どの href も完全な URI であって相対パスではない。

ページからソースへの結合は、サーバーがサイトの作りを知る必要のあるただ一つの場所だ。私たちのものはノートのローダの規則(docs/ の下の markdown のパスは /docs/ の URL になる、index はそのディレクトリを名指す、ja/ は言語の接頭辞)を言い直し、テストはその言い直しをローダ自身と、配信サイトの最後のクロールとに照合する。その照合が、二つの端の事例を仮定ではなく発見した方法だ。

データのリンクも同じように木から読む。URL のルートのファイルはアプリのフレームワークの慣例に従う。ページが読む記録は、そのファイルと、それが import するライブラリのモジュールに名前の出るデータファイルで、ライブラリだけを辿るので、サイトの枠のメニューと言語の表はページの読み物に数えない。記録の書き手は、それを名指すスクリプトだ。どれもソースの静的な読み取りなので、それらのリンクは inferred と言い、記録自身の注記が書き手を名指す場合だけが exact を得る。リンクの確度はクライアントが量るためのもので、だから私たちのリンクのすべてに付いている。

拒否が運ぶもの

拒否は JSON-RPC のエラーで、その data.reason でクライアントが分岐できる:

reason添えられるもの
not-found解決できる最寄りの URI、五つまで
out-of-root提供されているルート
denied何も無し。ファイルはあるが提供されない
too-largeサイズ、?range= と ?lines= のテンプレート、?as=stat のリンク
binary型と ?as=stat のリンク
redirect状態。訪問者なら先へ送られる
no-such-ref同じように始まる ref
git-errorgit 自身の最後の行
bad-facet、bad-cursor期待されていたもの

resolve ツールを通すと、同じ拒否は同じ欄をテキストに持つ isError の結果になるので、モデルも最寄りの URI を見る。

サーバーを試す

私たちのものは api/ で pytest の下に走り、いちばん大事なテストは新しいサーバーが写すべきものだ。パスはルートを離れる何物も綴れない(ドットの区切り、パーセントエンコードされたもの、外へのシンボリックリンク)。拒否されたファイルは redacted として一覧に載り、読もうとすれば拒まれる。ページの規則は、実際にページを組むものと一致する。外れた URI は最寄りの兄弟を返す。名前空間なしで組んだハンドラはそのどれも広告しない。何も無いところで通る検査は検査ではないので、どれも本物の拒否か本物の一覧に対して assert する。

まだ無いもの

まだ無いのは、購読と、書き込むもの全部。書き込みは計画していない。仕様が取っておいている方向は提案だ。クライアントがあればよいと思う URI を受け取り、レビューできる変更を仕立て、持ち主の承認なしには何も着地しないツール。