Skip to content

Latest commit

 

History

78 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mcp-server

ローカルのOllamaをMCPサーバーとして公開し、Claudeから作業を任せられるようにするNode.jsのサーバーです。 公式のMCP TypeScript SDK v2で作っています。

入口は次の2つです。 どちらもMCPの2026-07-28版(server/discover)と2025年版(initialize)の両方に応答します。

入口 用途 ファイルの読み込み 出力の保存
stdio(docker exec -i ollama-mcp node stdio.ts) 同じPCのClaude CodeとClaude Desktopから使います。こちらを勧めます 使えます(FILE_ROOTSの配下だけ) OUTPUT_DIRを設定したときだけ使えます
HTTP(POST /mcp、ポート3000) Cloudflare Tunnelを通して、claude.aiなどから使います HTTP_ALLOW_FILES=trueと認証を両方設定したときだけ使えます HTTP_ALLOW_WRITES=trueと認証とOUTPUT_DIRが要ります

構成

[ローカル]
Claude Code / Claude Desktop ──stdio──> docker exec ollama-mcp node stdio.ts ──> Ollama(ホストの11434番)

[リモート]
claude.ai ──HTTPS──> Cloudflare Access ──> Cloudflare Tunnel ──> 127.0.0.1:3000/mcp ──> Ollama

claude.aiとClaude Desktopのカスタムコネクタは、このPCではなくAnthropicのクラウドから接続します。 同じPCで使うだけなら、ローカル(stdio)の接続が確実です。 トンネルとAccessは要りません。

MCPのツール

ツール 内容 既定のモデル
ollama_chat 下書き、要約、翻訳などの作業を任せます。profileでphp、docker、git、code_reviewの定型の指示を選べます DEFAULT_MODEL
ollama_review_code コードを確かめます。行番号付きで「重大度、行、問題、改善案」を返します。取得したリポジトリとPull Requestの差分も確かめられます DEEP_MODEL
ollama_explain_error エラーやログの原因の候補と対処を返します DEEP_MODEL
ollama_list_models 入っているモデルの一覧を返します -
ollama_health Ollamaが動いているかと、サーバーの設定を返します -
ollama_job background: trueで受け付けた生成の状態と、保存先を返します。HTTPで保存が使えるときだけ出ます -
list_files 許可ルートの中のファイルとディレクトリを一覧します。filesに渡すパスを探すときに使います。ファイルを扱えるときだけ出ます -
read_file 1つのファイルを、ローカルのモデルに渡さずにそのまま読みます。save_outputで書いた結果を読み返すときに使います。ファイルを扱えるときだけ出ます -
git_clone GitHubのリポジトリをCLONE_ROOTの配下に取得します。owner/repoだけを受け、URLは受けません。CLONE_ROOTを設定したときだけ出ます -
git_read 取得したリポジトリの状態を読みます。status、log、diff、show、branches、remotesです -
git_write ブランチの作成、staging、commit、pushです。GIT_ALLOW_WRITE=trueにしたstdioでだけ出ます -
github_read Pull RequestとIssueと差分とコメントとチェックを読みます。失敗したCIの注釈とログの末尾も読めます(check_log)。GITHUB_MCP_TOKENを設定したときだけ出ます -
github_write Pull Requestの作成とコメントです。GITHUB_ALLOW_WRITE=trueにしたstdioでだけ出ます -
  • ファイルを扱えるとき(stdioと、設定したHTTP)は、files引数にWindowsの絶対パスを渡すと、サーバーがファイルを読み込みます。Claudeはファイルの中身を引数として書き出さずに済むため、トークンを節約できます
  • filesは、1つのパスのほかにグロブと行範囲も取ります
    • グロブはC:\dev\app\src\**\*.phpのように書きます。list_filesで探してから渡す往復を省けます
    • 行範囲はC:\dev\app\src\Main.php#L10-200のように末尾に付けます。GitHubの永続リンクと同じ書き方です。#L10は1行、#L10-は末尾までです
    • ディレクトリをそのまま渡すことはできません。グロブの書き方を添えて拒みます
  • inline_filesには、サーバーが読めないファイルの中身を{"name": ..., "content": ...}の形で渡します。許可ルートの外にあるファイルや、Claudeが別の環境で開いているファイルに使います
  • ollama_review_codeで差分を確かめるときは、差分を写さずに、出どころを渡します。サーバーが差分を取り、Claudeを通さずにローカルのモデルへ渡します
    • git_diffには取得したリポジトリを{"repo": "owner/repo", "ref": ..., "staged": ...}の形で渡します。CLONE_ROOTを設定したときだけ出ます
    • pull_requestにはPull Requestを{"repo": "owner/repo", "number": 12}の形で渡します。GITHUB_MCP_TOKENを設定したときだけ出ます
    • 差分の追加行と文脈の行には、新しいファイルでの行番号をサーバーが振ります。指摘は「ファイル:行」の形で返ります
    • 秘密のファイルはgit_readのdiffとgithub_readのpr_diffと同じ判定で外します。入力の予算に入らないファイルは丸ごと落とし、名前を応答とプロンプトの両方に書きます
  • CIの失敗はcheck_logで調べます
    • github_readのcheck_logは、Pull Requestのheadで失敗したチェック(failure、timed_out)ごとに、注釈と、GitHub Actionsのジョブのログの末尾200行を返します。行頭の時刻と色の制御文字は落とします
    • ログには、GitHubの伏せきれなかった秘密も混ざりえます。Claudeに読ませたくないときは、ollama_explain_errorにcheck_log: {"repo": "owner/repo", "number": 12}を渡します。ログはローカルのモデルにだけ渡り、応答には原因の候補だけが返ります
    • ログのAPIは保存先へのリダイレクトを返します。サーバーはリダイレクトを自分でたどり、保存先にはトークンを送りません
    • GitHubのAPIの呼び出しは、GITHUB_API_TIMEOUT(既定30秒)で打ち切ります
  • ollama_review_codeにstructured: trueを付けると、指摘をJSON(file、line、severity、problem、fix、uncertain)で受けます
    • Ollamaのformatで出力の形を絞ります
    • 渡していないファイルや、渡した行の範囲の外を指す指摘をサーバーが落とし、落とした数と理由を応答に書きます。差分では、番号を振った@@の範囲だけを通します
    • 残った指摘は、これまでと同じ形の文章と、MCPのstructuredContentの両方で返ります
    • 既定は付けない(文章のまま)です。outputSchemaは宣言しません。宣言すると、すべての応答にstructuredContentが要るためです
    • これはトークンを節約しません。contentの分はどちらにせよ払います。サーバーが読めるパスなら必ずfilesを使います
  • 渡せる量の上限は、文字数ではなくトークン数の目安で測ります。日本語のコメントが多いコードは1文字がほぼ1トークンになるためです
    • 既定の上限は約24000トークンで、コンテキストを32kトークンと見込んでいます。実際の長さはOllamaの設定(OLLAMA_CONTEXT_LENGTH、ModelfileのPARAMETER num_ctx)で決まり、サーバーからは見えません
    • OLLAMA_NUM_CTXを設定すると、その値をnum_ctxとしてOllamaに送り、上限もそこから出力の分と余白(2048)を引いた量にします
    • 入力が上限に張り付いたとき(prompt_tokensがコンテキスト長の9割以上)は、入力の一部が落とされた疑いとして警告します
    • ollama_healthのloaded modelsに、読み込み中のモデルと、Ollamaが返せば実際のコンテキスト長が出ます
    • 上限を超えた分は丸ごと落とし、落としたファイル名を応答とプロンプトの両方に書きます。黙って切りません
  • list_filesはpatternにグロブを取ります。大文字と小文字は区別しません
    • *は直下、**/*.phpは下の階層のPHPのファイル、*.{js,ts}は選択肢、末尾の/はディレクトリだけです
    • **でたどるのはpathから8階層までです。それより深いときは、そのことを結果に書き添えます
    • 秘密のファイルと、node_modules、vendor、.gitは出しません。シンボリックリンクはたどりません
  • modelには、モデルの名前の代わりに別名fast(DEFAULT_MODEL)とdeep(DEEP_MODEL)を渡せます。入っていないモデルを渡したときは、入っているモデルの一覧を添えて返します
  • 応答の末尾に[ollama] model=... prompt_tokens=... output_tokens=... done_reason=... elapsed=...が付きます。done_reason=lengthやdone_reason=timeoutのときは、出力が途中で切れています
  • 小さいモデルは同じ内容を繰り返し続けることがあるため、出力のトークン数に上限を設けています。ollama_chatは4096、ほかの2つは1536で、max_tokensで変えられます
  • ローカルのモデルの出力は誤りを含みます。Claudeの側で確かめてから使います

セットアップ

コンテナーを起動する

docker compose up -d --build

restart: unless-stoppedのため、Docker Desktopを起動すると一緒に立ち上がります。

Claude Desktopに登録する

%APPDATA%\Claude\claude_desktop_config.jsonのmcpServersに次を足し、Claude Desktopを終了してから起動し直します。 チャットとCodeタブの両方で使えます。

{
  "mcpServers": {
    "ollama": {
      "command": "docker",
      "args": ["exec", "-i", "ollama-mcp", "node", "stdio.ts"]
    }
  }
}

Claude Codeだけで使う場合は、次のコマンドでも登録できます。

claude mcp add --scope user ollama -- docker exec -i ollama-mcp node stdio.ts

サブエージェントを入れる

claude/agents/ollama-worker.mdを%USERPROFILE%\.claude\agents\に写すと、Claude Codeからollama-workerのサブエージェントとして呼べます。 Ollamaに作業を任せ、その結果をClaudeが確かめてから返すための指示です。

ツールを呼ぶたびの確認を省く場合は、%USERPROFILE%\.claude\settings.jsonのpermissions.allowにmcp__ollama__*を足します。 OUTPUT_DIRを設定しないかぎり、どのツールもファイルを書き換えません。 設定したときに書くのはOUTPUT_DIRの配下だけで、読み込みのツールは何も書き換えません。

環境変数

.envに書きます。 docker-compose.ymlは.envの値を差し込むことにだけ使い、次の変数だけをコンテナーに渡します。

変数 既定値 説明
OLLAMA_URL http://host.docker.internal:11434 OllamaのURLです
DEFAULT_MODEL nucbox-fast:latest ollama_chatの既定のモデルです
DEEP_MODEL qwen2.5-coder:14b コードの確認とエラーの解析の既定のモデルです
OLLAMA_TIMEOUT 300000 Ollamaから何も届かない状態の上限(ミリ秒)です。キューの待ち、モデルの読み込み、プロンプトの評価も含みます
OLLAMA_MAX_DURATION 3000000 1回の生成全体の上限(ミリ秒)です
OLLAMA_MAX_CONCURRENCY 2 同時に走らせる生成の数です。OllamaはGPUを1つずつ使うため、並べても全体は速くなりません
OLLAMA_MAX_QUEUE 8 待ち行列の長さの上限です。ここも一杯なら、待たせずにその場で断ります
OLLAMA_NUM_CTX 0 Ollamaに送るコンテキスト長(num_ctx)です。0なら送らず、Ollamaの設定に任せます。上げるとVRAMを多く使うため、載り切る値にします
HTTP_REQUEST_TIMEOUT 60000 HTTPの要求を受け取り終えるまでの上限(ミリ秒)です。応答を返している時間(生成の時間)には効きません
ALLOWED_HOSTS localhost,127.0.0.1,[::1],host.docker.internal,mcp.223n.tech HTTPで受け付けるHostとOriginです
HTTP_ALLOW_FILES false HTTPでもファイルの読み込みを許すかどうかです。認証(MCP_AUTH_TOKENかCF_ACCESS_*)がないときは無視します
MCP_AUTH_TOKEN なし 設定すると、HTTPにAuthorization: Bearer <値>を求めます
CF_ACCESS_TEAM_DOMAIN、CF_ACCESS_AUD なし 設定すると、HTTPにCloudflare AccessのJWT(Cf-Access-Jwt-Assertion)を求めます
CF_ACCESS_ALLOWED_EMAILS なし 設定すると、AccessのJWTのemailがこの一覧にある人だけを通します。カンマで区切って並べます。サービストークンのJWTにはemailがないため、拒まれます
FILE_ROOTS docker-compose.ymlで設定 ホストのパス=コンテナのパスを;で区切って並べます
OUTPUT_DIR なし ローカルのモデルの出力を書き出す先です。ホストのパス=コンテナのパスを1件だけ書きます。設定したときだけsave_outputが出ます
HTTP_ALLOW_WRITES false HTTPでも書き出しを許すかどうかです。HTTP_ALLOW_FILESとは別に持ちます。認証がないときは無視します
CLONE_ROOT docker-compose.ymlで設定 リポジトリを取得する先です。ホストのパス=コンテナのパスを1件だけ書きます。サーバーが書き換えてよいのはここの配下だけです
GIT_ALLOWED_OWNERS docker-compose.ymlで設定 取得してよいGitHubのownerです。カンマで区切って並べます。空なら取得そのものを拒みます
GIT_ALLOW_WRITE false commitとpushを許すかどうかです。stdioでだけ効き、HTTPでは常に無効です
GIT_TIMEOUT、GIT_MAX_DURATION 120000、600000 gitから何も届かない状態の上限と、1回の操作全体の上限です(ミリ秒)
GIT_USER_NAME、GIT_USER_EMAIL なし commitに使う名前とメールアドレスです。commitするなら両方とも要ります
GITHUB_MCP_TOKEN なし GitHubのAPIに使うトークンです。fine-grainedを使い、対象のリポジトリを列挙します
GITHUB_ALLOW_WRITE false Pull Requestの作成とコメントを許すかどうかです。stdioでだけ効き、HTTPでは常に無効です
GITHUB_API_TIMEOUT 30000 GitHubのAPIの1回の呼び出し(本文を読み終えるまで)の上限です(ミリ秒)。CIのログを読むときも使います
AUDIT_LOG_DIR docker-compose.ymlで設定 監査ログを1日1ファイルで書き出す先(コンテナーの中の絶対パス)です。空なら標準エラーにだけ出します。FILE_ROOTSの中は拒みます
AUDIT_RETENTION_DAYS 30 監査ログのファイルを残す日数です
  • タイムアウトしても、それまでに生成された部分はdone_reason=timeoutと警告を付けて返します
  • 無通信の上限(OLLAMA_TIMEOUT)は300秒のままです。全体の上限だけを延ばし、Ollamaが固まったときは早く気付けるようにしています
  • ollama_healthとollama_list_modelsの問い合わせは、OLLAMA_TIMEOUTと15秒の短いほうで打ち切ります。Ollamaが固まったときに、状態の確認そのものが300秒待たないようにするためです
  • HTTPのrequestTimeout(HTTP_REQUEST_TIMEOUT)は、要求を受け取り終えるまでの上限です。応答を返している時間には効かないため、長い生成のために上げる必要はありません。起動時に[http] request timeoutとして出します
    • 以前はOLLAMA_MAX_DURATION+60秒に合わせていましたが、Node 22と26で、requestTimeoutより長い応答が切れないことを確かめたうえで切り離しました。長くしておくと、本文をゆっくり送り続ける相手に、その間ずっと接続をつかまれます
  • 認証は、本文を読む前に確かめます。認証のない要求は、本文を解析せずに401を返します
  • 失敗した要求(状態コードが400以上)が同じ接続元から1分に60回を超えると、その接続元からの要求を1分ほど429で断ります。偽のトークンの連打で、重い認証の処理を回させないためです
    • 成功した要求は数えません。認証を通った普段の利用は妨げません
    • 接続元は相手のIPで見分け、X-Forwarded-Forは信じません。Cloudflare Tunnelを通る要求は、どれもcloudflaredから届くため、同じ枠を分け合います
    • 3000秒まで使えるのはstdioと、同じPCから直にHTTPを叩くときです。claude.aiのコネクタは約240秒、Cloudflareは無通信が約100秒で打ち切ります
  • MCP_AUTH_TOKENとCF_ACCESS_*の両方を設定したときは、どちらかを満たせば通します
  • どちらも設定しないと、このPCのほかのコンテナーからもhost.docker.internal:3000を通してHTTPを呼べます

動作を確かめる

curl.exe http://127.0.0.1:3000/healthz
docker logs --tail 20 ollama-mcp

HTTPのアクセスログには、メソッド、パス、ステータス、Host、JSON-RPCのメソッドが出ます。 本文は記録しません。 トンネルを通したリクエストがサーバーまで届いているかを確かめるときに使います。

リモートで使う

claude.aiから使うときは、Cloudflare TunnelとCloudflare Accessを前に置きます。

  1. Cloudflare Tunnelで、公開するホスト名(例: mcp.223n.tech)をhttp://127.0.0.1:3000に向けます
  2. Cloudflare Zero Trustで、そのホスト名に「Self-hosted」のAccessのアプリを1つだけ作ります
  3. アプリに「Allow」のポリシーを足し、使う人のメールアドレスを入れます
  4. アプリの「Managed OAuth」を有効にし、「Allowed redirect URIs」にhttps://claude.ai/api/mcp/auth_callbackを足します
  5. .envにCF_ACCESS_TEAM_DOMAINとアプリのCF_ACCESS_AUDを書き、コンテナーを作り直します
  6. claude.aiの「設定」の「コネクタ」で、https://<ホスト名>/mcpをカスタムコネクタとして足します
  • claude.aiとClaude Desktopのリモートのコネクタは、1回の呼び出しを約240秒で打ち切ります。Cloudflareは応答が約100秒途切れると打ち切ります。長い生成は、次の「長い生成をジョブにする」を使うか、ローカルで行います
  • うまくつながらないときはdocs/troubleshooting.mdを見てください

リモートでファイルを読む

HTTPでも、files引数とlist_filesを使えます。 認証がないまま有効にすると誰でもファイルを読めてしまうため、認証を設定したときだけ有効になります。

  1. 前の手順でCF_ACCESS_TEAM_DOMAINとCF_ACCESS_AUDを設定しておきます
  2. .envにHTTP_ALLOW_FILES=trueを足します
  3. 使う人をさらに絞るときは、.envのCF_ACCESS_ALLOWED_EMAILSにメールアドレスを書きます
  4. docker compose up -dでコンテナーを作り直します。ログにFile tools are enabled over HTTPと出れば有効です。CF_ACCESS_ALLOWED_EMAILSを書いたときは、ログの[auth]の行に登録した件数が出ます
  5. claude.aiの「設定」の「コネクタ」で、このコネクタのツールリストを更新します。list_filesが加わり、ollama_chatなどにfiles引数が付きます
  • 読めるのはFILE_ROOTSの配下だけで、秘密のファイルを拒むのはstdioと同じです
  • ファイルの中身はこのPCのOllamaにだけ渡ります。ただし、Ollamaの出力はclaude.aiに返るため、Anthropicのサービスを通ります
  • HTTP_ALLOW_FILESを外したときも、ツールリストを更新します。更新しないと、Claudeがなくなったツールや引数を呼んで失敗します
  • 大きなファイルを14Bのモデルに読ませると、1回の呼び出しの上限(約240秒)を超えることがあります。そのときはDEFAULT_MODELの7Bのモデルを使うか、ファイルを分けます
  • コネクタが約240秒で打ち切ったときは、途中まで生成された部分も返りません。長くなりそうな生成は、background: trueでジョブにします(次の節)

長い生成をジョブにする

HTTPで保存(OUTPUT_DIRとHTTP_ALLOW_WRITES=true)が使えるときは、生成のツールにbackground: trueを付けられます。 対象はollama_chat、ollama_review_code、ollama_explain_errorです。 付けると、受け付けた時点でジョブのIDを返します。 生成はクライアントの打ち切りを越えて続き、結果はOUTPUT_DIRに書かれます。

  1. background: trueを付けて呼びます。応答にジョブのIDが返ります
  2. ollama_jobにIDを渡して、状態(待ち、生成中、完了、失敗)を確かめます。IDを省くと、自分のジョブの一覧が返ります
  3. 完了すると、保存先のパス、先頭と末尾の抜粋、[ollama]の行が返ります。全文はread_fileで読みます
  • stdioでは使えません。クライアントが終わるとプロセスごと止まり、ジョブも消えるためです。stdioには打ち切りの問題もありません
  • ジョブは、ほかの呼び出しと同じ枠(OLLAMA_MAX_CONCURRENCYとOLLAMA_MAX_QUEUE)を使います。待ち行列が一杯なら、受け付けの時点で断ります
  • 生成中のジョブは、クライアントが切れても止めません。OLLAMA_MAX_DURATIONで必ず終わります
  • 終わったジョブの記録は1時間で消えます。結果のファイルはOUTPUT_DIRに残ります
  • 記録はプロセスのメモリに持ちます。コンテナーを作り直すと記録は消えますが、ファイルは残ります
  • ほかの識別子(Accessのメールアドレスなど)のジョブは読めません
  • 監査ログには、受け付けの記録とは別に、終わったときの記録(ツール名に:jobを付けたもの)が残ります

出力をファイルに保存する

長い下書きや翻訳は、save_outputでファイルに書き出せます。 応答にはパスと先頭と末尾の抜粋だけが返るため、Claudeが全文を読まずに済みます。

  1. ホスト側に書き出し先のディレクトリを作ります。コンテナーのnodeユーザーが書ける権限にします
    • docker-compose.ymlはC:\devを読み取り専用でマウントし、C:\dev\ollama-outだけを読み書きできる形で重ねています。別の場所にするときは、docker-compose.ymlのvolumesも合わせます
  2. .envにOUTPUT_DIR=C:\dev\ollama-out=/work/dev/ollama-outのように書きます
  3. HTTPでも使うときは、認証を設定したうえでHTTP_ALLOW_WRITES=trueを足します
  4. docker compose up -dでコンテナーを作り直します。ollama_healthのoutput savingがenabledになれば有効です
  • ファイル名はoutput_nameで指定します。使えるのは英数字と_と-だけで、.とパス区切りは拒みます
  • 拡張子はサーバーが.mdに決めます。.phpや.jsをサーバーに書かせないためです
  • すでにあるファイルは上書きせず、-2、-3と後ろに足して新しく作ります
  • 書き出したファイルの先頭には、ローカルのモデルが書いたものだという断りが入ります
  • 書き出したファイルはread_fileで読み返せます。#L120-200を付けると一部だけ読めます
  • ローカルのモデルは同じ行を繰り返して終わることがあります。末尾の重複を数え、疑わしいときは警告を付けます

MCPのリソースとして読む

ファイルを扱えるときは、MCPのresourcesとしても同じファイルを公開します。 resources/listは許可ルートだけを返し、resources/readはディレクトリなら一覧を、ファイルなら中身を返します。

  • URIはfile:///C:/dev/app/src/Main.phpの形です。#L10-200を付けると行範囲になります
  • 防御はfiles引数とまったく同じ経路を通ります。許可ルートの外、..、秘密のファイル、シンボリックリンクは同じように拒みます
  • resources/readにはツール名がないため、mcp__ollama__*の許可の対象になりません。そのぶん、ファイルのツールと完全に同じ条件でだけ公開します
  • resources/subscribeとページングには対応していません。一覧は許可ルートだけに絞っています

リポジトリを取得してgitとGitHubを操作する

CLONE_ROOTを設定すると、GitHubのリポジトリを取得して、そのままローカルのモデルにレビューさせられます。

  1. ホスト側に取得先のディレクトリを作ります(例:C:\dev\claude)
    • docker-compose.ymlはC:\dev\claudeを読み書きできる形でマウントしています。別の場所にするときは、docker-compose.ymlのvolumesも合わせます
  2. .envにCLONE_ROOTとGIT_ALLOWED_OWNERSを書きます
  3. privateのリポジトリを扱うときは、fine-grainedのトークンをGITHUB_MCP_TOKENに書きます。対象のリポジトリは列挙して絞ります
  4. commitとpushまで任せるときは、GIT_ALLOW_WRITE=true、GIT_USER_NAME、GIT_USER_EMAILを足します
  5. Pull Requestの作成まで任せるときは、GITHUB_ALLOW_WRITE=trueを足します
  6. docker compose up -d --buildでコンテナーを作り直します。ollama_healthで状態を確かめられます

privateリポジトリを取得する

SSHの鍵は要りません。 GITHUB_MCP_TOKENにfine-grainedのトークンを設定すると、HTTPS経由でそのまま取得できます。 サーバーはトークンをx-access-tokenのBasic認証としてgitに渡します。 子プロセスの環境変数だけで渡すため、argvに現れず、.git/configにも残りません。

  1. GitHubの「Settings」→「Developer settings」→「Personal access tokens」→「Fine-grained tokens」で発行します

  2. 「Resource owner」に、対象のリポジトリを持つ利用者か組織を選びます

  3. 「Repository access」は「Only select repositories」にして、使うリポジトリだけを選びます

  4. 「Repository permissions」を次のように設定します

    権限 必要な場面
    Metadata: Read 必須です。ほかの権限を選ぶと自動で付きます
    Contents: Read git_cloneです。pushもするならRead and write
    Pull requests: Read pr_list、pr_view、pr_diff、pr_commentsです。PRを作るならRead and write
    Issues: Read issue_list、issue_viewです。コメントするならRead and write
    Checks: Read pr_checksとcheck_log(注釈)です
    Actions: Read check_log(ログ)です。ollama_explain_errorのcheck_logも使います
  5. .envにGITHUB_MCP_TOKEN=github_pat_...と書き、docker compose up -dで作り直します

  6. ollama_healthのgithub apiがenabledになれば有効です

トークンを設定していないと、privateリポジトリの取得はRepository not foundで失敗します。 GitHubが認証のない要求に404を返すためで、名前の打ち間違いと見分けが付きません。 サーバーはこのとき、トークンが未設定であることを書き添えます。

  • 取得先はCLONE_ROOT/owner/repoです。パスはownerとrepoから組み立てるため、渡した文字列がパスの区切りとして働く余地がありません
  • URLは受け取りません。owner/repoだけを受け、https://github.com/owner/repo.gitはサーバーが組み立てます
  • 取得したリポジトリはlist_filesとfilesとread_fileから読めます。ローカルのモデルにレビューさせる目的なので、これは意図した動きです
  • 書き込みはstdioでだけ有効です。 GIT_ALLOW_WRITEとGITHUB_ALLOW_WRITEをtrueにしても、HTTP経由ではgit_writeとgithub_writeが出ません
  • main、master、developへの直pushは、設定にかかわらず拒みます。それらをheadにしたPull Requestの作成も拒みます
  • ghコマンドは入れていません。GitHubのRESTのAPIを直に呼ぶため、gh apiやgh aliasのような別の実行経路がそもそもありません

監査ログ

サーバーはファイルを書き、リポジトリを取得し、pushし、Pull Requestを作れます。 何が行われたかを後から言えるよう、ツールの呼び出しを1行1JSONで記録します。

{"ts":"2026-09-22T12:00:00.000Z","identity":"you@example.com","kind":"tool","tool":"git_write","ok":true,"ms":842,"args":{"repo":"223n/mcp-server","op":"push","branch":"feature/x"}}
  • 出力先は標準エラーです。stdioのとき標準出力はMCPの通信路なので、そちらには出しません

  • AUDIT_LOG_DIRを設定すると、標準エラーに加えてaudit-YYYYMMDD.jsonl(UTCの日付)へ1行ずつ追記します

    • stdioの入口はdocker execで起動する別のプロセスで、その標準エラーはClaude DesktopやClaude Codeの側に流れ、docker logsには残りません。書き込みのツール(git_write、github_write)はstdioでだけ出るため、その記録はこのファイルに残します
    • docker-compose.ymlは、名前付きボリュームaudit-logを/var/log/ollama-mcpにマウントし、既定でここに書きます。FILE_ROOTSの外に置き、ファイルのツールから読めないようにしています。FILE_ROOTSの中を指定すると、起動時に警告してファイルには書きません
    • HTTPとstdioの両方のプロセスが同じファイルに追記します。古いファイルは、HTTPのプロセスが起動時と1日ごとに消します(AUDIT_RETENTION_DAYS、既定30日)
    • 読むときはdocker exec ollama-mcp sh -c 'cat /var/log/ollama-mcp/audit-*.jsonl'です。jqを通すと絞り込めます
  • ローカルのモデルに生成を任せた呼び出しにはusageが付きます。実際に使ったモデル(別名は読み替えたあとの名前)、prompt_tokens、output_tokens、done_reason、枠を待った時間(queued_ms)です

    {"ts":"2026-09-26T12:00:00.000Z","identity":"stdio","kind":"tool","tool":"ollama_chat","ok":true,"ms":1200,"args":{"model":"fast"},"usage":{"model":"nucbox-fast:latest","prompt_tokens":4096,"output_tokens":301,"done_reason":"stop","queued_ms":0}}
    • どれだけ任せたかをモデルごとに数えるときは、次のようにします

      docker exec ollama-mcp sh -c 'cat /var/log/ollama-mcp/audit-*.jsonl' | jq -s 'map(select(.usage)) | group_by(.usage.model) | map({model: .[0].usage.model, calls: length, prompt_tokens: (map(.usage.prompt_tokens // 0) | add), output_tokens: (map(.usage.output_tokens // 0) | add)})'
    • ollama_healthは、そのプロセスが動き始めてからの合計を、モデルごとと識別子ごとに出します。stdioのプロセスはクライアントごとに起動し直されるため、長い期間はファイルで数えます

  • identityは、Cloudflare AccessのJWTのemail、サービストークンならservice:<クライアントID>、静的なトークンならtoken、stdioならstdioです。認証がない構成ではanonymousになります

  • argsには記録してよい鍵だけを残します。prompt、code、system、context、message、body、inline_filesの中身は出しません

    • 渡したファイルのパス(filesとpaths)は残します。何をローカルのモデルに渡したかは、監査でいちばん知りたいことだからです
    • inline_filesは件数だけにします。名前と中身のどちらも呼び出し側が決めるためです
  • resources/readにはツール名がなく、ツールの記録に載りません。読み取りの経路としては同じ重さなので、"kind":"resource"として別に記録します

  • HTTPの入口の記録は、docker logs ollama-mcpでも見られます。ログは10MBを3世代まで残します

同時に走らせる数を絞る

OllamaはGPUを1つずつ使うため、生成を並べて投げても待ち行列に並ぶだけで、全体は速くなりません。 待っている間もクライアントの上限(claude.aiは約240秒)は進みます。

  • OLLAMA_MAX_CONCURRENCY(既定2)までを同時に走らせ、それを超えた分はOLLAMA_MAX_QUEUE(既定8)まで待ち行列に並べます
  • 待ち行列も一杯のときは、待たせずにその場で断ります。Claudeを長く待たせず、早く判断できるようにするためです
  • 待っている間は、進捗の通知で「何件待ちか」を伝えます
  • 待ち時間まで含めて240秒を超えそうなときは、background: trueでジョブにします
  • 今の状態はollama_healthのconcurrencyに出ます

セキュリティ

  • .envはコミットしません。.gitignoreで外しています
  • Ollama(11434番ポート)には認証がありません。LANやインターネットへ直に公開しないでください
  • コンテナーは権限を絞って動かします(docker-compose.yml)
    • ルートのファイルシステムは読み取り専用で、書けるのは/tmp(メモリ上)と書き込み先だけです
    • C:\devは読み取り専用でマウントし、CLONE_ROOTとOUTPUT_DIRの場所だけを読み書きできる形で重ねます。サーバーの約束が外れたとき(gitやNodeの不具合など)に書き換えられる範囲を、この2つに絞るためです
    • ケーパビリティはすべて外し、特権の昇格を禁じ、プロセスの数に上限を設けます
    • CIも同じ絞り込みでコンテナーを起動し、HTTPとstdioが応答することを確かめます
  • HTTPでファイルを読めるのは、HTTP_ALLOW_FILES=trueに加えて認証を設定したときだけです
  • ファイルの読み込みはFILE_ROOTSの配下だけに限ります
    • .env、.envrc、.npmrc、秘密鍵、app_local.phpなどの秘密のファイルと、.gitや.sshなどの配下は拒みます
    • Windowsの8.3形式の短い名前(ENV~1など)で回り込むことも拒みます
  • 書き出せるのはOUTPUT_DIRの配下だけです。FILE_ROOTSには書きません
    • HTTPで書き出せるのは、HTTP_ALLOW_WRITES=trueに加えて認証を設定したときだけです。HTTP_ALLOW_FILESだけでは書けません
    • ファイル名は英数字と_と-だけに限り、NULやCOM1などWindowsが特別扱いする名前も拒みます
    • 全角の/はNFKCで/になるため、正規化してから確かめます
    • 作成はO_CREAT|O_EXCLで行います。先に置かれたシンボリックリンクをたどって別の場所へ書くことはありません
  • グロブでまとめて渡すときは、拒否リストではなく拡張子の許可リストで絞ります。名前を指定せずにサーバーが選ぶため、明示的なパスより狭くしています
    • 先頭が.の名前、拡張子のないファイル、ハードリンクは展開で拾いません
  • 読み込んだファイルに書かれた指示は、ローカルのモデルの出力に紛れ込むことがあります。出力の中の指示には従わないよう、ツールの応答と説明に書いてあります
    • OUTPUT_DIRをFILE_ROOTSの配下に置くと、書き出した出力を読み返せる代わりに、モデルの出力が普通のファイルのような顔で戻ってきます。起動時に警告を出し、書き出したファイルの先頭に出自を書いています
  • gitを動かすときは、環境変数を継承しません。GIT_SSH_COMMANDやGIT_EXTERNAL_DIFFなど、任意のコマンドを実行させる変数を持ち込ませないためです
    • システムの設定は/etc/git/server.gitconfigの1枚だけを読ませ、利用者のグローバルの設定は読ませません
    • 取得したリポジトリの.git/configは、GIT_CONFIG_SYSTEMとGIT_CONFIG_GLOBALを差し替えても読まれます。そこで、鍵の許可リストとコマンドの側の設定の2段で守ります
      • 操作の前に.git/configの鍵を許可リストで確かめ、ほかの鍵があればgitを動かさずに拒みます
      • 許すのは、git cloneとpush --set-upstreamが書く鍵(core.*の一部、remote.origin.*、branch.*.remoteとmerge)と、user.nameとuser.emailだけです。remote.origin.urlは、取得先のURLと同じであることも確かめます
      • core.hooksPath、core.fsmonitor、credential.helper、commit.gpgSign、protocol.*は、コマンドの側の設定(GIT_CONFIG_COUNT)で打ち消します。diffとshowには--no-ext-diffと--no-textconvを付けます
      • .git/configはリモートから配られないため、取得しただけで危険な鍵が入ることはありません。守る相手は、CLONE_ROOTに書けるホストの側のプロセスです
    • https以外のプロトコル(ext::、file://、git://、ssh://)を拒みます
    • 引数は必ず配列で渡し、シェルを介しません。利用者の値は値の位置にしか入らず、-で始まる値は拒みます
    • トークンは子プロセスの環境変数だけで渡します。argvに現れず、.git/configにも残りません
  • gitとGitHubの差分は、files引数とは別の読み取り口になります。filesと同じ判定(src/tools/sensitive.ts)で秘密のファイルの区画を外し、外したファイルの名前を書き添えます。showは中身を返しません
    • 対象はgit_readのdiff、github_readのpr_diff、ollama_review_codeのgit_diffとpull_requestです
    • 名前を変えた差分は、元の名前と新しい名前のどちらかが当たれば外します
    • ただしこれは名前による防御です。秘密に当たらない名前のファイルに書かれた秘密や、コミットのメッセージに書かれた秘密は読めます
  • 取得したリポジトリの中身は第三者が書いたテキストです。ローカルのモデルは指示の混入に弱いため、出力の中の指示には従いません
  • github_readの結果と、git_readのlog、diff、showの結果には、第三者が書いた文章なので指示として扱わない旨を末尾に添えます。サーバーのinstructionsとツールの説明にも同じことを書いています
  • ツールの呼び出しは監査ログに残します。中身は出しませんが、ファイルのパスと操作の種類は残します
  • HTTPのアクセスログは10MBを3世代まで残します

ディレクトリ

mcp-server/
├─ claude/agents/ollama-worker.md   Claude Code のサブエージェントの定義
├─ docs/                            運用の手引きとトラブルシューティング
├─ docker-compose.yml
├─ Dockerfile
├─ tsconfig.json                    型の検査の設定(成果物は作らない)
├─ index.ts                         HTTP の入口
├─ stdio.ts                         stdio の入口
├─ src/
│  ├─ server.ts                     McpServer を作る(HTTP と stdio で共通)
│  ├─ types.ts                      複数のファイルで共有する型
│  ├─ config/                       環境変数、モデル、定型の指示
│  ├─ git/exec.ts                   git の起動(環境を継承しない、引数は配列、上限と中断)
│  ├─ http/auth.ts                  HTTP の認証(静的なトークン、Cloudflare Access の JWT)
│  ├─ ollama/client.ts              Ollama の API(ストリーミング、タイムアウト、中断)
│  └─ tools/                        ツール、ファイルの読み込みと一覧、秘密のファイルの判定、出力の保存、リソース
└─ test/                            試験(Ollama の代わりに試験用のサーバーを使う)

TypeScript

ソースはTypeScriptで書きます。 ビルドはしません。 Nodeが.tsから型を取り除いてそのまま実行します(型の剥がし)。 そのためdist/のような成果物はなく、node index.tsとnode stdio.tsが本番の起動コマンドです。

この方法にはNode 22.18以上が要ります。 package.jsonのenginesがその下限を書いています。 Dockerのイメージが使うのはNode 26です。

型を検査する

Nodeは型を取り除くだけで、型が合っているかは見ません。 型の誤りが見つかるのは次のコマンドだけです。

npm run typecheck

npm run lintにも入っています。 CIでは「型の検査」ジョブが同じことをします。

書き方の決まり

設定はtsconfig.jsonにあり、次の3つが書き方を縛ります。

設定 何を縛るか
allowImportingTsExtensions importには実行時と同じ綴りを書きます(./files.tsであって./files.jsではありません)
verbatimModuleSyntax 型だけを取り込むときはimport typeと書きます。こう書かないとNodeが値の取り込みと区別できません
erasableSyntaxOnly enum、namespace、コンストラクターのパラメータープロパティは使えません。取り除くだけでは消えないためです

strictとnoUncheckedIndexedAccessを有効にしています。 arr[0]やobj[key]の型にはundefinedが入ります。 取り出した値は、そのまま使わずに確かめてください。

複数のファイルで使う型はsrc/types.tsに置きます。 MCPの通信で使う形は写さず、SDKの型(@modelcontextprotocol/server)をそのまま使います。

試験

npm testで試験します。 Ollamaの代わりに試験用のサーバー(test/helpers/mock-ollama.ts)を使うため、GPUとOllamaは要りません。

npm install
npm test

次のことを確かめます。

  • HTTPとstdioで、MCPの2025年版と2026-07-28版の両方につながること
  • ファイルの読み込みの防御(許可ルートの外、..、シンボリックリンク、秘密のファイル、大きさの上限)と、list_filesの絞り込み
  • 同じ防御が、グロブの展開とresources/readでも働くこと
  • 行範囲の切り出しで、行番号が元のファイルのまま振られること
  • inline_filesの名前で、見出しやフェンスを偽装できないこと
  • 書き出しの防御(パス区切り、..、二重の拡張子、Windowsの装置名、全角の区切り、置かれたシンボリックリンク)
  • 認証のないHTTPで、resourcesも書き出しの引数も出ないこと
  • owner/repoの検証(..、パスの区切り、Windowsの装置名、.gitで終わる名前、URL)
  • -で始まる値をgitの引数として拒むこと
  • 守るブランチへのpushと、それらをheadにしたPull Requestの作成を拒むこと
  • git_readのdiffとgithub_readのpr_diffから、filesが拒むのと同じ秘密のファイルが外れること(名前の変更、引用符で囲まれた名前を含む)
  • ollama_review_codeのgit_diffとpull_requestで、差分がローカルのモデルへのプロンプトにだけ入り、秘密のファイルが入らないこと。振った行番号が新しいファイルの行番号と一致すること
  • ollama_review_codeのstructuredで、渡していないファイルや範囲の外の行を指す指摘が落ち、付けたときだけformatとstructuredContentが使われること
  • check_logが失敗したチェックの注釈とログの末尾だけを返し、ログの保存先にトークンを送らないこと。ollama_explain_errorのcheck_logでログが応答に返らないこと。GitHubが応答を返さないときGITHUB_API_TIMEOUTで打ち切ること
  • background: trueの呼び出しがすぐにIDを返し、クライアントが切れたあとも生成が続いて保存されること。ほかの識別子からジョブを読めないこと、終わってから1時間で記録が消えること、stdioには出ないこと
  • 取得したリポジトリの.git/configに許可していない鍵(core.fsmonitor、core.hooksPath、diff.external、include.pathなど)があれば、gitを動かさずに拒むこと
  • 許可リストを通り抜けても、フックとcore.fsmonitorがコマンドの側の設定で止まること
  • github_readとgit_readのlog、diff、showの結果に第三者の文章だという断り書きが付き、statusと空の差分には付かないこと
  • HTTPではgit_writeとgithub_writeを出さないこと
  • 監査ログにpromptやcodeの中身が出ず、識別子とファイルのパスは出ること
  • resources/readで復号できないURI(壊れた符号化、NUL)を拒んだときも、監査ログに残ること
  • OLLAMA_NUM_CTXを設定したときだけnum_ctxを送り、入力の予算と上限の警告にその値を使うこと。ollama_healthが読み込み中のモデルを出すこと
  • 生成を任せた呼び出しの記録にusageが付き、ollama_healthがモデルごとと識別子ごとの合計を出すこと
  • 監査ログを日付ごとのファイルにも追記し、FILE_ROOTSの中の書き出し先を拒み、古いファイルだけを消すこと。CIでは、stdioの呼び出しの記録が名前付きボリュームに残ることを確かめます
  • 同時に走らせる数の上限と、待ち行列が一杯のときに断ること
  • すでに中断された呼び出しを待ち行列に並ばせないことと、枠を渡す間にも上限を超えて走らないこと
  • HTTPの認証(静的なトークン、Cloudflare AccessのJWT、メールアドレスの絞り込み)と、エラーの形
  • Cloudflare Accessの鍵の取得を、同時に届いた知らないkidのJWTで分け合い、失敗した直後は取り直さないこと
  • クライアントからの中断と、stdioのstdinが閉じたときに、Ollamaへの呼び出しが止まること
  • OLLAMA_MAX_DURATIONを超えたときに、途中までの出力を警告付きで返し、Ollamaへの呼び出しも止まること
  • HTTPのrequestTimeoutより長い生成が、途中で切れずに最後まで返ること
  • Ollamaが固まったとき、状態の確認が短い上限で打ち切られ、効いた上限の名前を知らせること
  • 認証を設定したHTTPで、認証のない要求には本文を読み終える前に401を返すこと
  • 失敗した要求が1分に60回を超えると429で断り、成功した要求は数えないこと
  • list_filesのグロブが、*を並べた意地の悪いパターンでもすぐ終わること(ReDoSを防ぐ)
  • ツールの説明に決め打ちのモデルの名前が出ず、別名fastとdeepが設定したモデルに読み替わること。入っていないモデルには一覧を添えて返すこと
  • サーバーが読む環境変数を、docker-compose.ymlがすべてコンテナーに渡していること
  • docker-compose.ymlがコンテナーの権限を絞り、C:\devを読み取り専用にして、書き込み先だけを読み書きできる形で重ねていること。CIが同じ絞り込みで起動すること
  • 環境変数の不正な値で、起動時に止まること

CIは、Node 22.18(enginesの下限)と最新の22と26で試験し、Dockerのイメージを作って起動したうえでHTTPとstdioの応答を確かめます。 あわせてTrivyでイメージの脆弱性を見ます。 apkで入れたパッケージの版はDependabotが追わないため、ここで拾います。 直せるもの(上流に修正がある高・重大)が見つかると失敗し、直せないものは記録に残すだけにします。

リポジトリの運用

ブランチの運用、リリース、ラベル、ワークフローはdocs/repository-operations.mdにあります。 変更の進め方はCONTRIBUTING.mdにあります。

変更したらnpm run lintを通します。 型の検査(tsc --noEmit)、Markdownの書式、日本語の書き方をまとめて確かめます。 文書だけを変えたときも型の検査が先に走ります。 ここで落ちたらnpm run typecheckを単体で実行し、どちらの検査が落ちたかを切り分けてください。

npm install
npm run lint

ライセンス

Apache License 2.0です。 LICENSEを見てください。

About

ローカルOllmaを参照、公開するMCPサーバー

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

Generated from 223n/repo_template