Theodolite について
Theodoliteは、エディターを使わない環境で言語サーバーにアクセスするためのヘッドレスMCPサーバーです。ワーカー、ゲート、サンドボックスエージェント向けに、固定された範囲のLSP読み取り機能と、明示的なゲート付きの編集機能を提供します。
主な機能
- 01固定された言語サーバーセッション
- 02LSP診断とコードナビゲーション
- 03シンボル、参照、ホバーの検索
- 04リネームのプレビューとコードアクションの一覧
- 05任意の許可リスト付き編集操作
- 06上限付きの整列済み結果と型付きエラー
活用シーン
- 01エディターなしのワーカーで診断を確認
- 02サンドボックスエージェントでソースを検索
- 03ファイル変更前にリネームをプレビュー
- 04許可されたクライアントによる編集の適用
MCP 対応
stdio経由で、限定されたLSP読み取りツールをMCPクライアントに公開します。変更ゲートが有効で、接続クライアントが許可リストにある場合に限り、編集ツールを登録します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
theodolite
ヘッドレスかつ決定論的な LSP ブリッジ MCP サーバー(IDEA-0092、
ai.qompass/theodolite)です。固定された言語サーバーセッションを、エディターを使わないコンテキスト(primo workers、
gates、sandbox agents)向けの制限付き読み取りツールとして公開します。
Theodolite は periscope(IDEA-0013)を補完します。periscope は人間が使用している Neovim を通じて応答しますが、theodolite は独自の固定サーバーセッションを管理し、エディターには一切触れません。
設計上の基準:~/workspace/lsp-mcp-study-20261025/SPEC.md。
ビルド記録:このディレクトリの BUILD-REPORT.md。
利用者向け指針(periscope / SCIP / phlow checks との振り分け、クライアント設定、変更に関する方針):docs/CHARTER.md。
提供機能(読み取りクラス、v1 + P3 読み取りクラスの完了)
ツール:session_status、diagnostics(プッシュによる蓄積と、サーバーが提供する場合のプルを統合し、診断ごとのチャネルの出所を保持)、hover、definition、declaration、
type_definition、implementation、references、
document_symbols、workspace_symbols、code_actions(一覧表示のみ。ペイロードは返却も実行もしません)、
rename_preview、session_restart。汎用の任意 LSP メソッド用ツール、edit_file、フォーマットツール、workspace/executeCommand のパススルーはありません。設計上、提供機能は固定リストに限定されています。
サーバー(コードではなく設定データ):bacon-ls(診断の担当)、 rust-analyzer(セマンティクス)、 lua-language-server(第 2 の実証用サーバー)。
ミューテーションクラス(P5、ゲート制御、デフォルトでは無効)
さらに2つのツールが存在し、ミューテーションゲートが有効な場合にのみ登録されます。code_action_apply と rename_apply です。rename_preview は読み取りクラスに属し、名前変更の編集セットを計算して表示するだけで、何も書き込みません。ゲートの適用対象ではありません。
- 設定。 設定には
mutation = "off"(デフォルト)またはmutation = "gated"と、空でないクライアント名のmutation_allowlistを指定します。不明な値、許可リストのないgated、offの状態での許可リストなど、その他すべての組み合わせでは、型付きエラーConfigInvalidを返して起動を拒否します。曖昧な設定が暗黙のうちにそのまま通ることはありません。 - 識別と登録。 ゲートが使用する識別情報は、MCP ハンドシェイクの
clientInfo.nameです。これはホストの信頼境界内でのルーティングラベル(SPEC §8)であり、認証レイヤーではありません。設定がgatedで、接続クライアントが許可リストに含まれている場合に限り、2つの apply ツールがtools/listに表示されます。それ以外の組み合わせでは登録されず、呼び出すとUnsupportedMethodを返します。ハンドシェイクに識別情報がない場合、gatedの設定ではフェイルクローズします。 - 判定。 すべての apply は、設定、識別情報、許可リスト、ハンドルの状態、バージョンの状態を受け取る純粋関数(
gate::decide)によって判定されます。許可・拒否を問わず、理由を含むすべての判定がセッションのトランスクリプトに記録されます(session_status→gate_decisions、メタデータのみ、サイズ上限あり)。 - ハンドル。
code_actionsは、編集を含む各アクションについて、クライアントに apply クラスが登録されている場合に限り、使い捨ての不透明なハンドルを発行します。編集内容と、リスト表示時に取得したすべての対象の SHA-256 スナップショットはサーバー側に保持されます。リスト表示にペイロードが含まれることはありません。code_action_applyが受け取るのはハンドルのみです。呼び出し元が編集内容を渡した場合は、型付きエラーを返して拒否します。apply 時に、偽造、使用済み、別セッションのハンドル、またはバージョンのずれが検出された場合は、型付きエラーGateDeniedを返し、ログに記録します。許可された apply は書き込み前にハンドルを消費するため、使用済みのハンドルで再試行することはできません。 rename_applyはハンドルではなく、プレビューの引数を受け取ります。最新の状態で再計算し、計算から書き込みまでの間に、追跡対象のすべてのドキュメントのハッシュを再検証します(ずれがあれば名前変更全体を拒否し、何も書き込みません)。プレビューがapplied: falseを返す場合、こちらはapplied: trueを返します。- 適用機構。 書き込み経路はブリッジの適用機構のみです。何も書き込む前に編集セット全体を検証します(すべての対象でワークスペース境界、存在、UTF-8、スパンの範囲、重複を確認)。その後、ファイルごとにアトミックに書き込みます(一時ファイルを同じディレクトリに作成して名前を変更し、元の権限を保持)。最後に、変更したファイルを再同期します。ファイルの作成・名前変更・削除操作は、すべての場所で型付きエラーとして拒否されます。ブリッジが編集するのはファイルの内容のみであり、サーバーから送信された
workspace/applyEditが適用されることはありません。
実行
cargo build --release
./target/release/theodolite --config bridge.toml
stdio 経由の MCP、NDJSON JSON-RPC です。無効な設定ではブリッジの起動を拒否します。設定の形式については bridge.toml.example を参照してください。対象マシン上の実際のバイナリパスを固定し、正確な --version の出力を指定してください。
特性
- 子プロセスは、環境をサニタイズしたうえで
setpriv --pdeathsig SIGKILLを介して絶対パスから起動します。次のセクションで説明するように、セッションごとにスーパーバイザーが1つ起動します。ブリッジに SIGKILL を送ると、すべてのセッションのプロセスツリーも停止します(テストスイートでは孤児化されたゲートを検証します)。 - セッションのキーは
(workspace, server)です。セッション数は最大4つです。停止したセッションが暗黙に再起動されることはなく、再起動にはsession_restartのみを使用します。 - すべての結果は正規の順序で並び、バイト数に上限があります。上限を超えた場合は、実際の項目数を含む型付きエラー
ResultTooLargeを返し、暗黙に切り詰めることはありません。エラーには安定したコードが付与されます(SPEC §4.6)。 - 準備完了の判定は状態に基づきます(initialize ハンドシェイクとサーバーごとの条件。rust-analyzer では、読み込みアクティビティが終了したことを確認し、プローブが成功する必要があります)。固定時間のスリープを準備完了の判定に使うことはありません。
シャットダウンとプロセスツリーの所有権(SPEC §3.6)
各セッションはスーパーバイザーのもとで実行されます。ブリッジは --supervise を指定して自分自身を再実行し、セッションの直接の子プロセスかつプロセスグループリーダーにします。スーパーバイザーは、そのグループ内でサーバーを起動します(setpriv --pdeathsig SIGKILL ラッパーによって、サーバーはスーパーバイザーに紐付けられるようになりました)。サーバーと、サーバーが残すデーモンはすべてスーパーバイザーのプロセスグループを共有し、スーパーバイザーはあらゆる経路でツリー全体を回収します。グループごとに /proc を列挙し、明示的な PID で kill し、負の PGID では決して kill しません。
- **トランスポート EOF/セッション終了/退去:**ブリッジは LSP の
shutdown+exitを送信し、shutdown_grace_msの間待機します。ツリーが残っている場合は、スーパーバイザーの stdin を閉じます。これにより EOF が発生し、スーパーバイザー独自の猶予時間の後にツリーを kill します。最後の手段は、スーパーバイザーに対するChild::killです。 - **サーバーの終了(原因を問わず):**スーパーバイザーはツリーの残りを kill し、サーバーのステータスを反映して終了します。そのため、ブリッジのリーパーは、直接起動した場合とまったく同じように終了を記録します。
- **ブリッジの SIGKILL/SIGTERM:**SIGKILL ではブリッジのコードを実行できず、SIGTERM ではハンドラーがインストールされていません(意図的なものであり、どちらの経路でも LSP のグレースフルな終了処理は実行できないことを、曖昧にせず明記しています)。保証はスーパーバイザーに委ねられます。各スーパーバイザーは 50 ms ごとに、ブリッジが
/procに存在するかどうかと記録された開始時刻をポーリングします。再利用された PID は生存中とは見なしません。ブリッジが消失した時点で、ツリーを kill します。
検証ゲート:tests/mcp_stdio.rs の tree_gate_* は、実際のデーモン孫プロセスを残すフェイクサーバーを実行し、セッションの再起動、トランスポート EOF、SIGKILL、SIGTERM のそれぞれの後、5 秒以内にサーバー、デーモン、スーパーバイザーがすべて終了することを確認します。P4 前のオーファン検証ゲート(サーバーのみ)も引き続き実行されます。
測定された制限を、そのまま制限として記載します。ブリッジに SIGKILL が送られた後のツリー回収は、スーパーバイザーの 50 ms のポーリングと /proc の走査に要する時間以内に行われますが、瞬時ではありません。また、スーパーバイザー自体が、EOF 経路さえ実行できないほど深刻に停止した場合、ブリッジの最後の手段で kill されるのはスーパーバイザーだけです。その経路では、原理上デーモンが生き残る可能性があります。ほかに既知の残存パターンはありません。P4 primo のスモークテスト(ヘッドレスバックエンドのデーモンがセッションツリー内にとどまる、実際の bacon-ls セッション)では、EOF、SIGKILL、SIGTERM のいずれの場合も、同じ 5 秒以内にデーモンが終了します。
テスト
cargo test # unit + stdio suites + parity harness
cargo clippy --all-targets -- -D warnings
cargo fmt --check
パリティゲート(tests/parity.rs)は、ブリッジの診断出力と単独で実行した bacon-ls の診断出力をバイト単位で比較します。bacon-ls バイナリが存在しない場合は、そのことを明確に通知してスキップし、primo では実際に実行します。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。