1. はじめに(背景と目的)
チーム開発においてIssueベースのタスク管理は基本ですが、「Issueを確認する → ブランチを切る → 作業する → コミットする → PRを出す」という一連の流れは、毎回同じ手順を踏む定型作業でもあります。
私たちの組織では、GitHubやGitLabのSaaS版ではなく、セルフホスト型のGitプラットフォーム Forgejo を採用しています。社内ネットワーク内でソースコードとIssueを完結して管理できることが選定の決め手でした。このForgejo上でIssue管理とKanbanボードを運用しています。
ここにAIコーディングアシスタント Claude Code を組み合わせれば、「Issueを読み取って、ブランチを切って、作業して、コミットまで」を一気通貫で自動化できるのでは?と考えました。
さらに、この仕組みは開発チームだけでなく、総務・バックオフィスチームのタスク管理やドキュメント作成にも導入しています。非エンジニアのメンバーがForgejoにIssueを起票するだけで、AIが必要なドキュメントの雛形を生成し、所定のディレクトリに配置してコミットまで完了する――プログラミングの知識がなくてもAI自動化の恩恵を受けられる環境を実現しました。
しかし、Claude CodeはデフォルトではForgejoのAPIにアクセスする手段を持っていません。そこで活用したのが MCP(Model Context Protocol) です。MCPサーバーを自作することで、Claude CodeにForgejo APIへの「目と手」を与え、Issueドリブンな開発ワークフローを実現しました。
2. システムの全体像と仕組み
アーキテクチャ図

仕組みのポイント:
- MCP(Model Context Protocol) は、AIアシスタントに外部ツールを提供するための標準プロトコルです。Claude Codeは
.mcp.jsonに定義されたMCPサーバーを起動し、そのサーバーが公開する「ツール」を呼び出せるようになります。 - 通信方式は stdio(標準入出力)。Claude Codeがサーバープロセスを子プロセスとして起動し、JSON-RPCでやり取りします。HTTPサーバーを立てる必要がなく、設定が非常にシンプルです。
- MCPサーバーは Forgejo REST API をラップし、4つのツール(
get_issue,list_issues,get_issue_comments,list_repos)を提供します。
3. 事前準備:ForgejoでAPIトークンを発行する
MCPサーバーがForgejo APIと通信するには、認証用のAPIトークンが必要です。以下の手順でForgejoからトークンを発行してください。
- Forgejoにログインし、画面右上のアバターアイコンから 「設定 (Settings)」 を開く。
- 左メニューから 「アプリケーション (Applications)」 を選択する。
- 「アクセストークンを管理」セクションにある 「トークンを生成 (Generate New Token)」 をクリックし、新しいトークンを発行する。
- トークン名: わかりやすい名前を付ける(例:
Claude MCP) - スコープ(権限): Issueの読み取りに必要な権限を付与する。最低限
read:issueを選択。リポジトリ一覧の取得も行う場合はread:repositoryも追加する。
- トークン名: わかりやすい名前を付ける(例:
- 「生成」ボタンを押すとトークン文字列が表示される。
注意: 発行されたトークン文字列は、この画面を閉じると二度と表示されません。必ずこの時点でコピーし、プロジェクトルートの
.envファイルに設定してください。
# .env に以下を追記
FORGEJO_TOKEN=ここに発行されたトークンを貼り付ける
4. 実装した主要ファイルとコードスニペット
4-1. MCPサーバー本体 (forgejo-mcp-server.js)
依存パッケージ(package.json)
{
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.28.0",
"node-fetch": "^3.3.2"
}
}
MCPサーバーの構築には公式SDK @modelcontextprotocol/sdk を使います。ESモジュール形式で記述するため "type": "module" が必須です。
環境変数の読み込み(dotenvを使わない軽量実装)
// .envファイルを手動パースする(dotenv不要)
const __dirname = dirname(fileURLToPath(import.meta.url));
try {
const envFile = readFileSync(resolve(__dirname, ".env"), "utf-8");
for (const line of envFile.split("\n")) {
const match = line.match(/^\s*([\w]+)\s*=\s*(.*)\s*$/);
if (match && !process.env[match[1]]?.trim()) {
process.env[match[1]] = match[2];
}
}
} catch {}
あえて dotenv を使わず、数行の手動パーサーで .env を読んでいます。依存を最小限に抑えるための判断です。
サーバーの初期化とツール定義
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server({ name: "forgejo", version: "1.0.0" }, {
capabilities: { tools: {} }
});
Server を生成し、capabilities: { tools: {} } で「このサーバーはツールを提供する」と宣言します。
ツール一覧の公開(ListTools)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_issue",
description: `Forgejo issueを取得する(対象: ${FORGEJO_OWNER}/${FORGEJO_REPO_PREFIX}* のみ)`,
inputSchema: {
type: "object",
properties: {
repo: { type: "string", description: "リポジトリ名(例: example-project)" },
issue_number: { type: "number", description: "Issue番号" }
},
required: ["repo", "issue_number"]
}
},
// ... list_issues, get_issue_comments, list_repos も同様
]
}));
ツール定義に description を日本語で書いているのもポイントです。Claude Codeはこの説明文を読んで、どのツールをいつ使うべきかを判断します。
ツールの実行ハンドラ(CallTool) — セキュリティチェック付き
// .env から読み込んだ環境変数
const FORGEJO_URL = process.env.FORGEJO_URL;
const FORGEJO_TOKEN = process.env.FORGEJO_TOKEN;
const FORGEJO_OWNER = process.env.FORGEJO_OWNER;
const FORGEJO_REPO_PREFIX = process.env.FORGEJO_REPO_PREFIX;
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
const headers = { Authorization: `token ${FORGEJO_TOKEN}` };
// リポジトリ名のプレフィックスチェック(意図しないリポジトリへのアクセスを防止)
function isAllowedRepo(repo) {
return repo.startsWith(FORGEJO_REPO_PREFIX);
}
if (name === "get_issue") {
const repo = args.repo;
if (!isAllowedRepo(repo)) {
return {
content: [{ type: "text", text: `エラー: リポジトリ "${repo}" は許可範囲外です。` }]
};
}
const res = await fetch(
`${FORGEJO_URL}/api/v1/repos/${FORGEJO_OWNER}/${repo}/issues/${args.issue_number}`,
{ headers }
);
const issue = await res.json();
return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] };
}
// ...
});
isAllowedRepo() によるプレフィックスチェック がセキュリティ上の重要なポイントです。AIが誤って関係ないリポジトリにアクセスすることを防ぎます。
サーバーの起動
const transport = new StdioServerTransport();
await server.connect(transport);
たった2行でstdio経由のMCPサーバーが起動します。
4-2. MCP設定ファイル (.mcp.json)
{
"mcpServers": {
"forgejo": {
"command": "node",
"args": ["./forgejo-mcp-server.js"]
}
}
}
このファイルをプロジェクトルートに置くだけで、Claude Codeが起動時に自動的にMCPサーバーを認識・起動します。command と args だけのシンプルな設定です。
4-3. 環境変数 (.env.example)
FORGEJO_URL=https://git.example.com
FORGEJO_TOKEN=your_forgejo_api_token_here
FORGEJO_OWNER=your-organization
FORGEJO_REPO_PREFIX=example-
FORGEJO_REPO_PREFIX を環境変数にしておくことで、プロジェクト名が変わっても設定変更だけで対応可能です。
5. AIを上手く動かすための工夫(CLAUDE.md の役割)
MCPサーバーで「Forgejoの情報を取得する能力」を与えただけでは不十分です。AIが正しい手順で、正しいルールに従って作業するためには、明確な指示書が必要です。それが CLAUDE.md です。
Claude Codeはプロジェクトルートの CLAUDE.md を自動的に読み込み、会話中ずっとコンテキストとして保持します。つまり、プロジェクト固有のルールをコードと一緒にバージョン管理できる「AIへの指示書」 です。
今回のプロジェクトで定義した主なルールを紹介します。
ルール1: Issue確認の義務化
1. Check Issues First: When asked to implement or design something,
proactively use the `get_issue` tool to fetch the relevant issue details
before generating files.
「まずIssueを読め」という指示です。これがないと、AIは口頭の指示だけで作業を始めてしまい、Issueに書かれた詳細な仕様やコメントの議論を見落とします。
ルール2: ブランチ名のゼロパディング
2. Create Branch Before Working: The branch name MUST follow the
`issue-NNN` format with a zero-padded 3-digit number
(e.g., `issue-001` for issue #1, `issue-042` for issue #42)
issue-1 ではなく issue-001。ゼロパディングを強制することで、ブランチの一覧がソートしたときにきれいに並びます。
ルール3: ドキュメントの配置先指定
3. Document Placement: Always place new deliverables in the appropriate
`docs/` subdirectory (`01_issues/`, `02_design/`, or `03_manuals/`).
Never create files in the root directory unless explicitly asked.
AIは何も言わないとルートディレクトリにファイルを作りがちです。明確にディレクトリ構造を指定しておくことで、成果物が散らばることを防ぎます。
ルール4: コミットメッセージでIssue参照
4. Commit Messages: Always include the issue reference
(e.g., `refs #5`) at the end of the commit message.
refs #5 をコミットメッセージに含めることで、ForgejoのIssueとコミットが自動的にリンクされます。
ルール5: pushの制御
5. No Direct Git Push: You may create branches and commit files,
but please ask the user before pushing to the remote repository.
AIが勝手にpushしないようにする安全弁です。ローカルでの作業は自由にさせつつ、外部への影響は人間の確認を必須にしています。
ルール6: MCP呼び出し時のパラメータ注意
6. Forgejo MCP Tool Required Parameters: When calling `get_issue` or
`list_issues`, you MUST always provide the `repo` parameter.
Omitting `repo` causes an `undefined.startsWith()` error.
実際にハマったバグをルールとして書き残した例です。AIが repo パラメータを省略して isAllowedRepo(undefined) でクラッシュする問題があったため、CLAUDE.mdに明記しました。実運用で発見したエラーをルール化してフィードバックするのがCLAUDE.md運用の肝です。
6. 実際のワークフロー
以下が、人間がIssueを起票してからAIがPR作成可能な状態まで持っていく一連のフローです。

ポイントは③〜⑥が全自動であることです。人間は「Issue #3 をやって」と言うだけ。AIがIssueの内容を読み、ブランチ命名規則に従い、正しいディレクトリにファイルを作り、規約通りのコミットメッセージで記録します。
⑦のpush前確認が安全弁として機能しており、「AIに任せきりで事故が起きる」リスクを抑えています。
7. まとめ
- MCPサーバーは驚くほど少ないコード量(164行)で作れる。stdio通信なのでインフラ不要
- CLAUDE.mdは「AIへのオンボーディングドキュメント」。新メンバー向けのREADMEと同じ感覚で書ける
- 実運用で踏んだ地雷をルール化するサイクル(ルール6のパラメータ問題など)が、仕組みの精度を上げる鍵
- Forgejo以外(Redmine, Backlog, 自社ツール等)でも、REST APIがあれば同じパターンで連携可能
