本文へ移動
AI Dev Lab Japan AI開発ツール、AIコーディングエージェント、M...

TypeScriptでGitHub read-only MCPサーバーを作る:Fine-grained PATとtool allowlistの最小構成

TypeScriptでGitHub read-only MCPサーバーを作る:Fine-grained PATとtool allowlistの最小構成の判断ポイントを表す抽象サムネイル

3行まとめ

Visualread-only GitHub MCPの境界GitHubから読む範囲と、最初から作らない操作を分けて見ます。
  1. 1MCP client

    許可されたtoolだけを呼び出します。

  2. 2read-only MCP server

    Issue、Pull Request、repository contentsをGETだけで扱います。

  3. 3GitHub REST API

    Fine-grained PATで許可されたrepositoryと権限の範囲だけを読みます。

  4. 4対象外

    Issue作成、PRコメント、file update、workflow起動は含めません。

read-onlyはGitHub上の状態を変えない境界であり、秘密情報や未信頼入力への注意は別に必要です。

GitHub連携MCPは、最初からIssue作成、PRコメント、ファイル更新まで渡すより、Issue、Pull Request、repository contentsを読むだけのread-only構成から始める方がレビューしやすいです。

ただし、read-onlyは「GitHub上の状態を変更しない」という意味であって、秘密情報の露出、未信頼入力、過剰なrepository閲覧、rate limit、監査不足の問題は残ります。

この記事では、2026-06-04に公式情報を確認したうえで、@modelcontextprotocol/sdk 1.29.0、GitHub REST API、Fine-grained personal access tokenを前提に、TypeScriptで最小のread-only MCPサーバーを組む判断軸をまとめます。

この記事で作るもの

作るのは、ローカルのstdioで起動するTypeScript製MCPサーバーです。MCPクライアントから呼べるtoolは、GitHub REST APIのGET系だけに絞ります。

  • github_list_issues
  • github_get_issue
  • github_list_pulls
  • github_get_pull
  • github_get_contents

この5つを上限の目安にします。汎用のgithub_requestは作りません。便利ですが、method、endpoint、owner/repo、pathの境界が一気に曖昧になるからです。

この記事で作らないもの

Issue作成、PRコメント、merge、file update、GitHub Actions dispatch、Webhook受信、remote MCP hosting、OAuth app化、GraphQL対応は扱いません。

特に、POST/PATCH/DELETEを含む連携は別記事で扱うべき範囲です。この記事の目的は「AIエージェントにGitHubを読ませるとき、どこまで狭く始められるか」を判断することです。

この記事でわかること

VisualPoC前に決める設計項目コードを書く前に、権限、tool、入力、ログ、失敗条件を分けて確認します。
PATの範囲

対象repositoryとContents、Issues、Pull requestsのread権限を決めます。

toolの範囲

MCPで公開するtool名を少数に固定します。

contentsの範囲

repository contentsはpath allowlistでさらに絞ります。

監査と失敗条件

403、404、rate limit、token未設定、巨大ファイルを区別して扱います。

読後のゴールは、read-only GitHub MCPを社内PoCへ入れる前の判断順序を持つことです。

この記事で持ち帰れるのは、コード断片そのものよりも、read-only GitHub MCPを社内PoCに入れる前の設計順序です。

  • Fine-grained PATで最初に絞るべきrepositoryと権限
  • MCPのtool名を少数に固定する理由
  • GitHub REST APIをGET endpointだけに包む実装方針
  • repository contentsを読むときのpath allowlist
  • 監査ログに残す情報と残さない情報
  • 403、404、rate limit、token未設定、巨大ファイルなどの失敗条件
  • read-onlyでも人間レビューを外さない方がよい場面

MCP自体の基礎から確認したい場合は、先に<a href="https://ai-dev.blog.mo-gmo.com/mcp-tools-resources-prompts-permission-design/">MCPとは何か:Tools・Resources・Promptsと権限設計</a>を読むと、Tools、Resources、Promptsの役割を整理しやすくなります。

前提知識

VisualMCP Toolを権限境界として見るtool定義は、AIに何を外部取得させるかを決める入口です。
tools/list

クライアントが利用できるtool一覧を取得します。

tools/call

指定されたtoolをinput schemaに沿って実行します。

tool定義

tool名、description、input schema、handlerに権限設計が表れます。

npm stable

この記事では@modelcontextprotocol/sdk 1.29.0を基準にします。

GitHub連携では、toolを増やすほどAIが触れられる情報面も広がります。

MCPのToolは、AIエージェントが外部操作や外部取得を呼び出す入口です。公式のTools仕様では、クライアントがtools/listでtool一覧を取得し、tools/callで指定toolを実行する流れになります。

つまり、MCPサーバーのtool定義は「AIに何をさせるか」の境界そのものです。GitHub連携であれば、tool名、description、input schema、handlerの中に、権限設計がそのまま表れます。

read-onlyは安全の完成形ではない

read-only MCPは、GitHub上の状態を書き換えません。これは大きな利点です。PRコメントを書かない、Issueを作らない、ファイルを更新しない、workflowを起動しない、という境界を作れるからです。

一方で、Issue本文、PR本文、README、設計書、設定ファイルは、LLMに渡る未信頼入力です。本文の中に「これまでの指示を無視して」型の文章が混ざっていても、MCPサーバーはそれを「上位指示」として扱ってはいけません。詳しくは<a href="https://ai-dev.blog.mo-gmo.com/ai-coding-agent-prompt-injection-issues-docs-mcp-guardrails/">AIコーディングエージェントのprompt injection対策</a>で整理しています。

今回はnpm stableのSDKを基準にする

2026-06-04時点で、npm view @modelcontextprotocol/sdk version1.29.0でした。一方、公式GitHub repositoryのmainブランチ上のpackage.json2.0.0-alpha.0でした。

この記事では、読者が再現しやすいようにnpm stableの@modelcontextprotocol/sdk@1.29.0を基準にします。mainブランチのalpha情報は、更新確認の対象として扱い、本文コードの前提にはしません。

npm view @modelcontextprotocol/sdk version

記事内のサンプルは、Node.jsのESM、TypeScript、zod/v4、stdio transportを想定します。社内標準がCommonJSや別のruntimeに寄っている場合は、importと実行コマンドを調整してください。

なぜGitHub MCPをread-onlyから始めるのか

Visualread-only、dry-run、writeの違いPoCで見るべき境界を、GitHubへの副作用とレビュー粒度で比べます。
項目内容見方
read-onlyGitHubへの副作用はなく、何を読んだかを中心にログを残します。
dry-run直接の変更はありませんが、生成した差分や対象の確認が必要です。
writeIssue作成、コメント、pushなどがあり得るため、操作前承認と詳細ログが必要です。

セキュリティ上の比較なので勝敗ではなく、初期PoCで説明しやすい境界から選びます。

GitHubは、AIコーディングエージェントにとって非常に強い情報源です。Issue、PR、diff、README、コード、設定、CIログに触れられると、調査や修正提案の質が上がります。

その反面、権限を広く渡すと、AIエージェントは開発チームの作業面に深く入り込みます。最初のPoCでは、便利さよりも境界の説明しやすさを優先した方が、レビュー、導入判断、停止判断が楽になります。

段階GitHubへの副作用人間承認ログ粒度最初のPoC適性
read-onlyなしtool許可とログレビューから始めやすい何を読んだかを中心に残す高い
dry-run直接の変更はないが変更案を生成する変更案の確認が必要生成した差分や対象を残す中程度
writeIssue作成、コメント、pushなどがあり得る操作前承認が必須誰が何を変えたかまで必要低い

GitHub連携は便利さより権限境界が先に問題になる

X/Twitter上でもMCP、ブラウザ操作、agent権限、GitHub連携に近い話題は継続的に見られます。ただし、2026-06-04時点では、指定された2アカウントの直近72時間投稿を公開検索から安定して取得できませんでした。この記事ではXを事実ソースに使わず、需要シグナルとして「MCPと外部tool権限への関心が続いている」程度に留めます。

実務で重要なのは、話題性ではなく、次の問いに答えられることです。

  • このMCPサーバーは、どのowner/repoを読めるのか
  • どのGitHub REST endpointを呼べるのか
  • tokenにどのrepository permissionを与えたのか
  • AIに渡した本文は、未信頼入力として扱われるのか
  • 後から「何を読もうとしたか」を追えるのか

read-onlyでもIssueやdocsは未信頼入力である

read-only MCPで一番見落としやすいのは、取得対象が「読むだけのデータ」ではなく、AIへの入力になる点です。

Issue本文には、ユーザー、外部協力者、bot、過去の開発者が書いた文章が混ざります。READMEやdocsにも、古い運用手順やサンプルの秘密情報が残っていることがあります。AIエージェントに読ませるなら、次のように扱うべきです。

  • GitHubから取った文章は参考情報であり、上位指示ではない
  • Issue本文の指示より、リポジトリ内のAGENTS.mdやシステム側ルールを優先する
  • file contentsを丸ごとログに残さない
  • .env、秘密鍵、credential、production dumpのようなpathは読ませない
  • allowlist外のrepositoryやpathは、AIが要求しても拒否する

注意点

read-onlyは副作用を小さくする設計であって、情報管理を不要にする設計ではありません。

最小構成の全体像

Visual4つの責務に分けるローカルstdioの最小構成を、レビューしやすい部品に分解します。
MCP server

toolを定義し、stdioでクライアントと接続します。

GitHub REST client

GitHub APIへGETだけを送り、methodとheadersを固定します。

Allowlist

owner、repo、pathを検証し、許可対象以外をAPIへ渡しません。

Audit log

読み取り試行と結果を残し、tokenや本文全量は残しません。

remote公開やOAuth化より先に、ローカルで境界を説明できる形にします。

今回の構成は4つに分けて考えると、レビューしやすくなります。

部品役割境界
MCP servertoolを定義し、stdioでクライアントとつなぐ公開toolを少数に固定する
GitHub REST clientGitHub APIへGETだけを送るmethodとheadersを固定する
Allowlistowner/repo/pathを検証する許可対象以外をAPIへ渡さない
Audit log読み取り試行と結果を残すtokenや本文全量は残さない

stdioローカル実行に閉じる

最初のPoCでは、remote MCP serverとして公開しない方が扱いやすいです。stdio transportなら、MCPクライアントがローカルコマンドとしてサーバーを起動します。HTTP公開時に必要な認証、CORS、ネットワーク境界、セッション管理、サーバー監視を後回しにできます。

TypeScript SDKの最小構成は、既存の<a href="https://ai-dev.blog.mo-gmo.com/typescript-mcp-server-tools-resources-prompts-minimal/">TypeScriptでMCPサーバーを作る記事</a>でも扱っています。今回はその上に、GitHub REST API、token、allowlist、監査ログを足すイメージです。

GitHub API clientはGETだけにする

実装上は、tool handlerごとにfetchを書くより、GitHub用の小さなclientを作り、そこにGET固定の制約を置く方が安全です。

type GitHubGetOptions = {
  path: string;
  token: string;
  query?: Record<string, string | number | undefined>;
};

async function githubGet({ path, token, query = {} }: GitHubGetOptions) {
  const url = new URL(`https://api.github.com${path}`);
  for (const [key, value] of Object.entries(query)) {
    if (value !== undefined) url.searchParams.set(key, String(value));
  }

  const response = await fetch(url, {
    method: "GET",
    headers: {
      "Accept": "application/vnd.github+json",
      "Authorization": `Bearer ${token}`,
      "X-GitHub-Api-Version": "2022-11-28",
      "User-Agent": "ai-dev-lab-github-readonly-mcp",
    },
  });

  return response;
}

この関数にmethod引数を持たせないのがポイントです。将来write系を追加するなら、別関数、別tool、別レビューにします。

確認コマンド

最小構成の確認は、成功ケースより先に境界を見ます。

node --version
npm view @modelcontextprotocol/sdk version
npm install @modelcontextprotocol/sdk@1.29.0 zod
npm install -D typescript tsx @types/node

次に、GITHUB_TOKEN未設定時に起動を止める、allowlist外repoを拒否する、allowlist外pathを拒否する、という失敗ケースを通します。

Fine-grained PATはrepoと権限を小さく作る

VisualPATで最初に絞る範囲GitHub側の権限を、repositoryとpermissionの両方で小さくします。
項目内容見方
Resource owner個人またはorganizationのどちらのrepositoryを読むかを先に決めます。
Repository access最初は検証用の1 repositoryに限定します。
Contents readREADMEや設定ファイルも読めるため、path allowlistと組み合わせます。
Issues / Pull requests read一覧と詳細のGETに必要な範囲だけを許可します。
Metadatarepository名や基本情報として見える範囲も前提に含めます。

Fine-grained PATはGitHub側の境界であり、MCPサーバー側のallowlistとは別に確認します。

GitHub REST APIをprivate repositoryに対して使うなら、認証情報が必要です。この記事では、最初のPoCとしてFine-grained personal access tokenを使う前提で説明します。

GitHub AppsやOAuth Appが適している組織もありますが、最小構成の学習とPoCでは、Fine-grained PATの方が「どのrepoに、どの権限を与えたか」を確認しやすいです。

Resource ownerとRepository accessを先に絞る

Fine-grained PATでは、resource ownerとrepository accessを選びます。最初から全repositoryを対象にせず、検証用の1repositoryに限定してください。

設定の初期値

項目推奨する初期値理由
Resource owner検証対象のuserまたはorganizationどの所有者のrepositoryを読めるかを固定する
Repository accessOnly selected repositories対象repoを1つから始める
Expiration短めに設定PoC後の放置を避ける
Token name用途がわかる名前監査と棚卸しをしやすくする

organization repositoryでは、組織側の承認ポリシーによりtokenがすぐ使えない場合があります。ここで詰まったら、MCP実装の問題ではなくGitHub側の認可設定の問題として切り分けます。

Issues、Pull requests、Contentsはreadに限定する

今回のtoolに必要なrepository permissionは、最小では次の3つです。

GitHub RESTで読む対象使うendpoint例Fine-grained PAT権限
Issue一覧/詳細GET /repos/{owner}/{repo}/issuesGET /repos/{owner}/{repo}/issues/{issue_number}Issues: Read
Pull Request一覧/詳細GET /repos/{owner}/{repo}/pullsGET /repos/{owner}/{repo}/pulls/{pull_number}GET /repos/{owner}/{repo}/pulls/{pull_number}/filesPull requests: Read
ファイル内容GET /repos/{owner}/{repo}/contents/{path}Contents: Read

GitHub DocsのFine-grained PAT権限表では、endpointごとに必要な権限を確認できます。公開前にこの表を見直すのが大事です。GitHub側の権限表は変わり得るため、社内手順書に落とす場合も確認日を残してください。

Metadataは見える範囲として扱う

GitHubのAPIでは、基本的なrepository metadataが関係する場面があります。ここを「何か足りないから権限を全部readにしよう」と誤解しないようにします。

権限不足でAPIが失敗したときは、response headerのX-Accepted-GitHub-Permissionsや対象endpointのドキュメントを確認し、必要な権限だけを増やします。最初からContents、Issues、Pull requests以外を広く付ける必要はありません。

注意点:Contents readは強い

Contents readは便利ですが、強い権限です。対象repositoryに秘密情報が置かれていれば、読めてしまう可能性があります。

GitHub上に秘密情報を置かないのが前提ですが、MCP側でもpath allowlistで補完します。

  • README.md
  • docs/
  • src/
  • package.json
  • tsconfig.json

たとえば最初はこの程度に絞り、.env.pem.keysecrets/dump/backup/node_modules/dist/、巨大ファイル、binary fileは拒否します。

Tool allowlistはMCPの外側と内側で二重に置く

Visual二層allowlistの考え方クライアント側の許可とサーバー内部の制約を重ねます。
  1. 1client許可

    利用者環境で呼べるtoolを絞ります。

  2. 2公開tool名

    github_list_issuesなど、用途が読める少数のtoolに固定します。

  3. 3repo検証

    owner/repoをserver側で確認し、対象外はAPIへ送る前に拒否します。

  4. 4path検証

    contents取得はdocs/やREADME.mdなど、許可したpathだけに絞ります。

クライアント設定は利用者環境に依存するため、サーバー側の制約を最後の境界として持ちます。

MCPクライアント側にtool許可設定がある場合でも、サーバー側のallowlistは必要です。クライアント設定は利用者環境に依存します。サーバー側の制約は、実装として常に効く最後の境界です。

公開するtool名を少数に固定する

tool名は、権限レビューの単位です。名前だけで用途が読めるようにします。

tool名目的作らない代替
github_list_issuesIssue一覧を読むgithub_search_everything
github_get_issueIssue詳細を読むgithub_request
github_list_pullsPR一覧を読むgithub_api
github_get_pullPR詳細を読むgithub_mutate_pull
github_get_contents許可pathのファイルを読むgithub_get_any_file

汎用toolは、使う側には便利です。しかし、監査する側から見ると「何ができるtoolなのか」を毎回handlerの中まで読まなければいけません。AIエージェント向けのtoolは、狭く、名前で意図がわかる方が扱いやすいです。

owner/repoをserver側で検証する

owner/repoは、MCPクライアントやLLMから渡された文字列をそのままAPI URLに入れないでください。環境変数や設定ファイルで許可repositoryを固定し、入力値が一致した場合だけGitHub APIへ進めます。

const allowedRepos = new Set(
  (process.env.GITHUB_ALLOWED_REPOS ?? "")
    .split(",")
    .map((repo) => repo.trim())
    .filter(Boolean),
);

function assertAllowedRepo(owner: string, repo: string) {
  const key = `${owner}/${repo}`;
  if (!allowedRepos.has(key)) {
    throw new Error(`Repository is not allowed: ${key}`);
  }
  return key;
}

失敗時のtool resultでは、詳細な内部設定を返さず、「このrepositoryはallowlist外です」と短く返します。監査ログには、試行されたowner/repo、tool名、時刻、拒否理由を残します。

path allowlistでcontents取得をさらに絞る

GET /repos/{owner}/{repo}/contents/{path}は、使い方を間違えるとrepository内の広い範囲をAIに渡します。pathもserver側で絞ります。

失敗条件

const allowedPathPrefixes = ["README.md", "docs/", "src/", "package.json"];
const blockedPathFragments = [".env", ".pem", ".key", "secrets/", "backup/"];

function normalizeContentPath(input: string) {
  const path = input.replace(/^\/+/, "");
  if (!path || path.includes("..") || decodeURIComponent(path).includes("..")) {
    throw new Error("Invalid path");
  }
  if (blockedPathFragments.some((fragment) => path.includes(fragment))) {
    throw new Error("Blocked path");
  }
  if (!allowedPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix))) {
    throw new Error("Path is not allowed");
  }
  return path;
}

URL encodeで..を回避しようとする入力もあるため、decode後も確認します。binary fileや大きすぎるfileは、GitHub APIのresponseを見てから拒否します。

GitHub REST APIはGET endpointだけを薄く包む

VisualtoolとGET endpointの対応tool追加時に、method、endpoint、入力、失敗時の扱いを確認します。
項目内容見方
github_list_issuesGET /repos/{owner}/{repo}/issuesでIssue一覧を読みます。
github_get_issueGET /repos/{owner}/{repo}/issues/{issue_number}でIssue詳細を読みます。
github_list_pullsGET /repos/{owner}/{repo}/pullsでPR一覧を読みます。
github_get_pullGET /repos/{owner}/{repo}/pulls/{pull_number}でPR詳細を読みます。
github_get_contentsGET /repos/{owner}/{repo}/contents/{path}で許可pathのファイルを読みます。

汎用のgithub_requestを作らず、endpointごとにレビューできる薄いtoolへ分けます。

toolとendpointの対応を表にしておくと、レビューが楽になります。

tool名methodendpoint例主な入力失敗時の扱い
github_list_issuesGET/repos/{owner}/{repo}/issuesstate、labels、per_page403/404/rate limitを分ける
github_get_issueGET/repos/{owner}/{repo}/issues/{issue_number}issue_number存在しない番号は短いエラーにする
github_list_pullsGET/repos/{owner}/{repo}/pullsstate、base、head、per_pagePR read不足をログに残す
github_get_pullGET/repos/{owner}/{repo}/pulls/{pull_number}pull_numberfiles取得は別呼び出しでもよい
github_get_contentsGET/repos/{owner}/{repo}/contents/{path}path、refpath allowlistとsize上限を見る

IssueとPull Requestは一覧と詳細に分ける

一覧toolは、AIが調査対象を絞るために使います。詳細toolは、番号指定で本文やmetadataを読むために使います。

レスポンスを丸ごと返す必要はありません。最初は次の程度に整えます。

  • number
  • title
  • state
  • user.login
  • created_at
  • updated_at
  • html_url
  • bodyの先頭数千文字まで

bodyを短く切るのは、token節約だけでなく、未信頼入力の影響範囲を小さくする意味もあります。必要なら「本文の続きが必要か」を人間に確認させる運用にできます。

repository contentsはファイル取得を中心にする

最小構成では、directory listingよりfile取得を中心にした方が扱いやすいです。directory listingを許すと、AIが探索範囲を広げやすくなります。

file取得では、次を確認します。

  • responseのtypefile
  • encodingbase64
  • sizeが上限以内か
  • allowlist内pathか
  • binary fileではないか
  • refを許可するなら、branch/tag/shaの扱いを決めているか

最初はrefを省略し、default branchの許可pathだけにしても構いません。branch比較や過去commit参照は便利ですが、PoC初期の説明責任が増えます。

REST client層でheadersを固定する

GitHub REST APIでは、認証にAuthorization: Bearerを使い、API version headerとしてX-GitHub-Api-Versionを送る形が公式ドキュメントで案内されています。サンプルでは2022-11-28を明示します。

rate limitも早めに見ます。GitHub Docsでは、unauthenticated requestのprimary rate limitは60 requests/hour、personal access tokenなどのauthenticated requestは通常5,000 requests/hourとして説明されています。実運用ではsecondary rate limitもあるため、403をすべて権限不足と決めつけないでください。

TypeScript SDK 1.29.0でMCP serverを組み立てる

Visual最小実装のファイル分担handlerにすべてを詰め込まず、責務ごとに分けます。
package.json

@modelcontextprotocol/sdkとZodの依存を固定します。

server.ts

tool登録とstdio接続を持ち、descriptionにread-onlyの意図を入れます。

allowlist.ts

owner/repo/pathの許可条件を集中させます。

github.ts

GitHub REST APIへのGET requestとheadersを扱います。

audit.ts

tool名、対象、status、request idをJSONLで残します。

LLM向けの短いエラーと、ログ向けの調査情報は分けて扱います。

最小のファイル構成は、次のように分けると読みやすくなります。

github-readonly-mcp/
  package.json
  tsconfig.json
  src/
    server.ts
    github.ts
    allowlist.ts
    audit.ts

package.json

SDKとZodを固定します。ここでは、2026-06-04確認時点のstableである@modelcontextprotocol/sdk@1.29.0を使います。

{
  "name": "github-readonly-mcp",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "dev": "tsx src/server.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.29.0",
    "zod": "^4.1.12"
  },
  "devDependencies": {
    "@types/node": "^22.15.3",
    "tsx": "^4.19.4",
    "typescript": "^5.8.3"
  }
}

server.tsの骨格

tool登録では、descriptionに「read-only」「GET only」「allowlisted repository only」を入れておくと、クライアント側のtool一覧でも意図が伝わります。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod/v4";

const server = new McpServer({
  name: "github-readonly-mcp",
  version: "0.1.0",
});

server.registerTool(
  "github_get_issue",
  {
    title: "Read GitHub issue",
    description: "Read one issue from an allowlisted GitHub repository. GET only.",
    inputSchema: {
      owner: z.string().min(1),
      repo: z.string().min(1),
      issueNumber: z.number().int().positive(),
    },
  },
  async ({ owner, repo, issueNumber }) => {
    // ここでrepo allowlist、GitHub GET、audit logを呼ぶ
    const issue = {
      owner,
      repo,
      number: issueNumber,
      note: "Replace this object with the normalized GitHub REST response.",
    };

    return {
      content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

実務用には、handler内へ直接ロジックを書き込まず、allowlist.tsgithub.tsaudit.tsへ分けます。tool handlerは、入力検証済みの値を渡し、失敗時に安全なtool resultを返す薄い層にします。

エラーはLLM向けとログ向けに分ける

AIへ返すエラーは短くします。内部設定、token、GitHub response body全文、file content全文は返しません。

function toolError(message: string) {
  return {
    isError: true,
    content: [{ type: "text" as const, text: message }],
  };
}

監査ログには、もう少し調査しやすいmetadataを残します。

{
  "at": "2026-06-04T09:30:00.000Z",
  "tool": "github_get_contents",
  "repo": "octo-org/example",
  "path": "src/index.ts",
  "method": "GET",
  "status": 200,
  "requestId": "github-request-id",
  "rateLimitRemaining": "4987",
  "result": "ok"
}

残すのは「何を読もうとしたか」と「結果を調査できるmetadata」です。本文全量を残すと、監査ログが別の情報漏えい源になります。

監査ログは「誰が何を読もうとしたか」を追える粒度にする

Visualログに残すもの、残さないもの成功ログだけでなく、拒否ログと権限不足を後から見直せる粒度にします。
項目内容見方
残すtool名、owner/repo、pathまたはnumber、HTTP method、status、実行時刻を残します。
GitHub側の手がかりGitHub request idとrate limit情報を、調査できる範囲で残します。
重視するallowlist外repo、allowlist外path、権限不足、rate limitの拒否ログを見ます。
残さないtoken、Issue本文全量、file content全量、response body全文は残しません。

監査ログの目的は、何を読めたかよりも、拒否すべき入力が拒否されたかを確認することです。

read-only MCPで監査ログが必要なのは、成功を誇るためではありません。拒否すべきアクセスが拒否されたか、権限不足が起きたか、rate limitに当たったかを後から見直すためです。

成功ログより拒否ログを重視する

最初に見るべきログは、成功したGETではなく拒否ログです。

イベント残す情報残さない情報
allowlist外repotool名、owner/repo、拒否理由token、許可repo一覧全量
allowlist外pathtool名、repo、path、拒否理由file content
権限不足status、GitHub request id、endpoint種別response body全文
rate limitstatus、remaining、reset、request idtoken
成功tool名、repo、path/number、statusIssue本文やfile content全量

ログはJSONLで1行1イベントにすると、後で集計しやすいです。保存期間、ローテーション、保存先、アクセス権も決めます。

GitHub request idとrate limit情報を残す

GitHub APIのresponse headerには、request idやrate limit関連の情報が含まれます。障害調査では役に立ちますが、LLMへ渡す必要はほとんどありません。

MCP tool resultには「GitHub APIがrate limitに達しました。しばらく待って再試行してください。」程度を返し、ログにはstatus、request id、remaining、resetを残す、と分けてください。

tokenと本文全量をログに残さない

監査ログへtokenを残さないのは当然として、Issue本文やfile content全量も避けます。read-only MCPは情報を読むためのサーバーです。ログが巨大な二次保管庫になると、別の管理対象が増えてしまいます。

必要なら、本文のhash、文字数、先頭の短い抜粋だけを残します。抜粋を残す場合も、個人情報や秘密情報が入る可能性を考えて、最初のPoCではmetadataだけにするのが無難です。

失敗条件を先に決めてから動作確認する

Visual成功デモの前に見る失敗条件実務では、通ってほしい入力よりも落ちてほしい入力の確認が重要です。
項目内容見方
token未設定起動時または初回callで安全に失敗し、ログで確認できるようにします。
権限不足Issues readやPull requests read不足をstatusとrequest idで追えるようにします。
対象repo外GitHub APIへ行く前にallowlistで拒否します。
404 / rate limit存在しない対象とrate limitを同じエラーにまとめません。
contents制約巨大ファイル、binary contents、path allowlist外を短いエラーで扱います。

失敗条件を先に決めると、PoCが単なる成功デモで終わりにくくなります。

GitHub連携MCPの検証で、成功デモだけを見ると危険です。実務で重要なのは、失敗すべき入力が失敗することです。

tokenと権限の失敗

まず、認証情報まわりを落とします。

条件期待する結果見る場所
GITHUB_TOKEN未設定起動時または初回callで安全に失敗tool result、stderr、audit log
token期限切れ認証失敗として返すstatus、request id
対象repo外GitHub APIへ行く前に拒否audit log
Issues read不足Issue toolだけ失敗status、accepted permissions
Pull requests read不足PR toolだけ失敗status、accepted permissions
Contents read不足contents toolだけ失敗status、accepted permissions

ここで、すべての失敗を「tokenが間違っています」にまとめない方がよいです。権限不足、repo対象外、rate limit、存在しないresourceは、運用上の意味が違います。

GitHub RESTの失敗

GitHub APIでは、403や404が複数の意味を持つことがあります。private repoで権限が足りない場合、見え方が直感と違うこともあります。

失敗時は、本文ではなくmetadataで判断します。

  • HTTP status
  • GitHub request id
  • rate limit remaining/reset
  • X-Accepted-GitHub-Permissions
  • 呼び出したtool名
  • owner/repo/pathまたはnumber

403を「権限不足」と決め打ちせず、rate limitとsecondary rate limitも候補に入れてください。

入力とcontentsの失敗

repository contentsでは、入力検証が特に大事です。

  • allowlist外path
  • ..を含むpath
  • URL encodeで..を含むpath
  • 先頭/でroot風に見せるpath
  • 巨大ファイル
  • binary file
  • directoryをfileとして扱うケース
  • base64 decode失敗

成功ケースより先に、これらが拒否されることを確認してください。

結果:最初のPoCで見るべき合格ライン

VisualPoCの合格ラインread-only GitHub MCPを次の段階へ進める前に確認する条件です。
tool一覧

tools/listにGitHub read-only toolだけが出ます。

GET固定

すべてのtoolがGET endpointだけを呼びます。

API前拒否

allowlist外のowner/repoはGitHub APIへ行く前に拒否されます。

失敗の区別

token未設定、権限不足、rate limit、404を分けて扱えます。

ログ確認

tool名、repo、path/number、status、request idが残ります。

うまくいかなかったときは、tool一覧、allowlist、PAT、REST response、audit logの順に見ます。

この記事の構成で最初のPoCを行うなら、合格ラインは次の通りです。

評価基準

  1. MCPクライアントのtools/listに、GitHub read-only toolだけが出る。
  2. すべてのtoolがGET endpointだけを呼ぶ。
  3. allowlist外のowner/repoはGitHub APIへ行く前に拒否される。
  4. contents取得は許可pathだけに限定される。
  5. token未設定、権限不足、rate limit、404を区別して扱える。
  6. audit logにtool名、repo、path/number、status、request idが残る。
  7. token、Issue本文全量、file content全量がログに残らない。

ここまで通れば、「AIにGitHubを読ませる」PoCとしてはかなり小さく始められます。逆に、ここが通らない状態でwrite系toolを足すのは早いです。

うまくいかなかったときに見る順番

動かないときは、次の順番で見ます。

  1. npm view @modelcontextprotocol/sdk versionで記事と同じstableを見ているか。
  2. MCP serverがstdioで起動しているか。
  3. GITHUB_TOKENが環境変数として渡っているか。
  4. Fine-grained PATのrepository accessが対象repoを含むか。
  5. Issues、Pull requests、Contentsのread権限が足りているか。
  6. owner/repo/path allowlistで弾いていないか。
  7. GitHub APIのrate limitやsecondary rate limitに当たっていないか。

SDKやGitHub権限の仕様は変わるため、記事のコードをそのまま社内標準にせず、確認日とバージョンを手順に残してください。

実務で使うなら

VisualPoCから運用へ広げる順番個人、チーム、本番で見るべき条件を分けます。
  1. 個人PoC

    対象repoを1つ、toolを5つ以下、GETのみ、stdioのみ、ログはJSONLにします。

  2. チームPoC

    tokenの所有者、保管場所、review担当、ログ閲覧者を決めます。

  3. 運用前

    権限レビュー、ログレビュー、token rotation、停止基準を確認します。

  4. 拡張時

    write系やremote化は、承認と監査の設計を先に増やしてから検討します。

便利さを増やすほど、権限境界と人間レビューの設計も増やします。

個人PoC、チームPoC、本番運用で、見るべきものを分けます。

個人PoC

個人PoCでは、対象repoを1つ、toolを5つ以下、GETのみ、stdioのみ、ログはJSONL、という制約にします。

この段階では、便利さを増やすよりも、拒否条件を見る方が価値があります。AIに「このrepo以外を読んで」と頼んで拒否されるか、secrets/を読もうとして拒否されるか、token未設定で安全に失敗するかを確認してください。

チームPoC

チームPoCでは、tokenの所有者と保管場所が重要になります。

  • 誰がFine-grained PATを作るのか
  • organization repoでは承認が必要か
  • tokenをどこに置くのか
  • ローテーション周期はどうするのか
  • 退職や異動時に誰が無効化するのか
  • allowlist変更はPRレビューにするのか
  • audit logを誰が見るのか

ここを曖昧にしたままMCPを配ると、read-onlyでも運用が属人化します。法人導入の権限設計は、<a href="https://ai-dev.blog.mo-gmo.com/enterprise-ai-coding-permission-design-api-keys-private-repos-human-approval/">APIキー、非公開リポジトリ、人間承認フローの記事</a>も参考になります。

本番運用へ広げる条件

本番運用へ広げるなら、少なくとも次を満たしてから対象repoを増やします。

  • security reviewを通した
  • tool allowlistの変更手続きがある
  • path allowlistの変更手続きがある
  • audit logの保存期間とアクセス権が決まっている
  • token rotationが運用に入っている
  • prompt injectionを未信頼入力として扱うルールがある
  • write系toolは別設計、別承認にしている

read-only MCPは、write系へ進む前の土台として使うと強いです。最初から万能GitHub toolを作るより、読ませる範囲を説明できるMCPサーバーにしておく方が、チームでレビューしやすくなります。

セキュリティ・コスト注意

Visual初期構成で避けたい形PAT、allowlist、監査ログ、token消費を別々に見ます。
広すぎるPAT

全repositoryや不要な権限を最初から渡さないようにします。

汎用request tool

github_requestのような広いtoolでmethodやendpointを自由にしません。

contentsの返しすぎ

Issue本文やfile content全量をそのまま返すと、contextと費用を使います。

監査不足

拒否ログ、status、request idがないと、後から判断できません。

PATはGitHub側の権限、allowlistはMCPサーバー側の境界、監査ログは後から確認する証跡です。

セキュリティ面では、Fine-grained PAT、allowlist、監査ログの3つを分けて見ます。PATはGitHub側の権限、allowlistはMCPサーバー側の境界、監査ログは後から確認するための証跡です。どれか1つだけでは足りません。

コストの見方

コスト面では、GitHub API自体のrate limitと、LLM側のtoken消費を見ます。Issue本文やfile contentを丸ごと返すと、LLM側のcontextと費用を使います。body抜粋、file size上限、必要なfileだけの取得で抑えてください。

やってはいけない初期構成

最初の実装で避けたいのは、次の形です。

  • github_requestのような汎用toolを作る
  • tool引数で任意methodを受け取る
  • tool引数で任意URLを受け取る
  • owner/repo allowlistがない
  • path allowlistがない
  • tokenをログに出す
  • response body全文を監査ログに残す
  • 403をすべて権限不足と決めつける
  • read-onlyだから人間レビュー不要、と扱う

AIエージェントに外部toolを渡すと、便利さはすぐ上がります。だからこそ、最初は退屈なくらい狭い構成で始める方が長持ちします。

よくある迷いどころ

Visual判断に迷う3つの論点既存サーバー、GraphQL、自動実行の扱いを短く整理します。
既存GitHub MCP

個人利用や一時調査では早い一方、repoやpathの境界を明文化したい場合は専用実装も選択肢です。

GraphQL

柔軟ですが、最小構成ではREST GETの方がendpointと権限の対応を追いやすいです。

自動実行

read-onlyでも未信頼入力を読むため、承認なしで広く自動実行する前に範囲を絞ります。

read-onlyは判断を軽くする材料であり、人間レビューを不要にする理由ではありません。

公式や既存のGitHub MCPではだめですか

既存サーバーを使う選択肢はあります。特に個人利用や一時的な調査では、すでに整備されたMCPサーバーの方が早いこともあります。

ただし、企業やチームで「このrepoの、このpathだけ読ませる」「監査ログにこの項目だけ残す」「write系は存在させない」といった境界を明文化したいなら、自作のread-only専用サーバーは選択肢になります。

GraphQLを使わない理由は何ですか

GraphQLは柔軟です。一方、最小構成ではREST GETの方が、endpointとFine-grained PAT権限の対応を追いやすいです。

この記事の目的は、GitHubから効率よくあらゆる情報を取ることではありません。toolごとの権限境界を読みやすくすることです。GraphQLは、read-only REST構成で運用ルールが固まってから検討しても遅くありません。

read-onlyなら承認なしで自動実行してよいですか

最初から完全自動にする必要はありません。read-onlyでも、private repoの情報、Issue本文、設計資料、file contentsをAIに渡します。

初期PoCでは、tool allowlist、repo allowlist、path allowlist、監査ログを確認しながら使い、チームの合意が取れた範囲だけ自動実行に寄せる方が安全です。

次に読むなら

MCPやGitHub権限まわりは更新が速い領域です。更新通知を追いたい場合は、<a href="https://ai-dev.blog.mo-gmo.com/newsletter/">ニュースレター</a>でAI開発基盤の記事更新を確認できます。自社のMCP設計、Fine-grained PAT、tool allowlist、監査ログを一緒に見直したい場合は、<a href="https://ai-dev.blog.mo-gmo.com/contact/">お問い合わせ</a>から相談してください。

参照した主な情報源

  • https://modelcontextprotocol.io/docs/sdk – Model Context Protocol SDKs
  • https://github.com/modelcontextprotocol/typescript-sdk – Model Context Protocol TypeScript SDK
  • https://modelcontextprotocol.io/specification/latest/server/tools – Model Context Protocol Tools specification
  • https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens – GitHub Docs: Managing your personal access tokens
  • https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens – GitHub Docs: Permissions required for fine-grained personal access tokens
  • https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api – GitHub Docs: Authenticating to the REST API
  • https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28 – GitHub Docs: Rate limits for the REST API

更新履歴

Visual確認した公式情報の時点記事内の前提がいつ確認されたものかを明確にします。
  1. 2026-06-04

    @modelcontextprotocol/sdkのnpm stableが1.29.0であることを確認しました。

  2. 2026-06-04

    TypeScript SDK repository mainが2.0.0-alpha.0であることを確認しました。

  3. 2026-06-04

    GitHub REST API、Fine-grained PAT権限、REST API認証、rate limitの公式情報を確認しました。

SDKやGitHubの仕様は変わるため、実装時は現在の公式情報も確認します。

日付内容
2026-06-04初版。@modelcontextprotocol/sdkのnpm stableが1.29.0であること、TypeScript SDK repository mainが2.0.0-alpha.0であること、GitHub REST API、Fine-grained PAT権限、REST API認証、rate limitの公式情報を確認しました。