Forgejo × Claude Code:MCPで自律型AIアシスタントにIssue駆動タスクをさせてみた

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からトークンを発行してください。

  1. Forgejoにログインし、画面右上のアバターアイコンから 「設定 (Settings)」 を開く。
  2. 左メニューから 「アプリケーション (Applications)」 を選択する。
  3. 「アクセストークンを管理」セクションにある 「トークンを生成 (Generate New Token)」 をクリックし、新しいトークンを発行する。
    • トークン名: わかりやすい名前を付ける(例: Claude MCP
    • スコープ(権限): Issueの読み取りに必要な権限を付与する。最低限 read:issue を選択。リポジトリ一覧の取得も行う場合は read:repository も追加する。
  4. 「生成」ボタンを押すとトークン文字列が表示される。

注意: 発行されたトークン文字列は、この画面を閉じると二度と表示されません。必ずこの時点でコピーし、プロジェクトルートの .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サーバーを認識・起動します。commandargs だけのシンプルな設定です。

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があれば同じパターンで連携可能

Recruit

ディーメイクでは各ポジションで一緒に働く仲間を募集中! エンジニア、デザイナー、ディレクターなど、多彩な職種があります。
一緒に成長していきましょう!

  • URLをコピーしました!