【アドベントカレンダー2025】Jira のコンテキストを Copilot に読ませようとして MCP サーバーを自作したが「ツールの引力」には勝てなかった話

はじめに

こんにちは。ぐるなびウエディング開発チームの渡邊です。普段はバックエンドエンジニアとして開発・運用を担当しています。

この記事では、Jira を GitHub Copilot の Agent Mode(MCP)から参照できるように MCP サーバーを自作してみた話を書きます。
作って動かすところまではできた一方で、最終的には本格運用までは至らず、個人 PoC に近い状態で止まりました。

  • どんな MCP tools を作ったのか(ReadOnly で絞った話)
  • Jira 連携で地味に面倒だったところ(customfield 周り)
  • なぜ「作ったのに使われなくなった」のか(GitHub 側の導線の強さ/運用の現実)

ブラウザ往復、地味に効く

VSCode で実装している最中に、仕様確認のためにブラウザで Jira を開く。
チケットを探して、説明を読んで、必要そうな部分を拾って、エディタに戻る。

この往復が地味にストレスで「これ、Copilot が IDE 内で取ってきてくれたら楽なのに」とずっと思っていました。

GitHub Copilot の Agent モード(MCP)を触り始めた頃に、ふと

「Jira を参照できる MCP サーバーがあれば、画面切り替えを減らせるのでは?」

と思い、Jira 向けの MCP サーバーを(最小機能で)自作してみました。

結論としては 「動いたけど、使われるところまで持っていけなかった」 です。
ただ、その理由は「実装が甘かった」だけではなく、Copilot を取り巻く ツールの“レール”の強さや運用・審査・保守の現実にありました。

目指した体験:雑に聞いて、裏で Jira を引いてほしい

狙ったのは、特定のコマンドを覚えて呼び出すことではなく、普段どおりに話しかけた結果として、
必要なら Copilot が裏側で Jira を参照してくれる体験です。

たとえば:

  • 「今の作業って、何やるんだっけ?」
  • 「検索基盤まわりのタスクって、何か残ってた?」
  • 「自分にアサインされてる未完了のタスクってどんなものがある?」

こういう質問に対して、Copilot が裏側で Jira MCP を使って情報を拾ってくる。
これができると「Jira を開きに行く」回数が減って、集中が切れにくくなるはず、という発想でした。

また、ブランチ名にチケットキーを入れている運用だったので、
「このブランチ、何の対応だっけ?」のような雑な問いからチケット内容を引けることも期待していました。

作ったもの:Jira 用 MCP サーバー(ReadOnly / tools 3つ)

自作にした理由は、当時「これをそのまま使えばいい」という形が見つからなかったからです。
公式のものはクラウド前提で、社内環境の Jira を読む用途には当てはめにくい。一方で Community 実装は、業務利用でいきなり採用するには不安が残りました。
また、認証情報を Copilot に渡して自由に API を叩かせるのも怖かったので、ReadOnly に絞った最小構成で自作することにしました。

こだわったのは、Copilot が叩ける範囲を絞ってガードレールを作ることです。

  • 認証:Jira の Personal Access Token(PAT)
  • 機能:ReadOnly 相当に絞る(作成・更新・遷移・コメント等はやらない)
  • 形:VSCode から stdio で起動する MCP サーバー

実装した tool は 3 つです。

  • my_active_issues:自分にアサインされた未完了チケット一覧
  • search_issues:検索(内部的には JQL)
  • get_issue:特定チケットの詳細

実装の雰囲気:FastMCP + Zod で tools を登録する

実装は TypeScript + FastMCP を使いました。
PoC だと、MCP サーバーの定型(tool 定義〜起動)を手早く書けるのがありがたかったです。

search_issues の部分だけ抜粋すると、やっていることはこんな感じです(概略):

server.addTool({
  name: 'search_issues',
  description: 'JQLクエリで課題を検索する',
  parameters: z.object({
    jql: z.string().describe('JQLクエリ'),
    includeCompleted: z.boolean().default(false),
    maxResults: z.number().default(20),
  }),
  execute: async ({ jql, includeCompleted, maxResults }) => {
    // project を固定して検索範囲を絞る(暴れないようにする)
    let fullJql = `project = "${config.projectKey}" AND (${jql})`
    if (!includeCompleted) fullJql += ` AND (${config.incompleteStatusJql})`

    const data = await searchJiraIssues(fullJql, maxResults)

    // Copilot が読みやすいように Markdown に整形して返す
    return formatSearchResults(data, '検索結果')
  },
})

ポイントは 2 つで、

  • 検索範囲を固定して、想定外のプロジェクトに飛ばないようにする
  • API レスポンスをそのまま返すのではなく、Markdown に整形して “読む用の形” に寄せる

あたりを意識していました。

Jira 連携で面倒だったところ:customfield を「人間が解読」する

Jira の REST API で課題を取ると、customfield_***** が大量に返ってきます。
これをそのまま LLM に渡しても、ノイズが多くてあまり嬉しくありません。

そこで「必要なものだけ」調べて、表示できるようにしました。
※ 社外公開のため、カスタムフィールドの ID や具体例は一部ぼかしています。以降は説明用のダミー表記です

例:

  • customfield_XXXXX:Sprint 情報
  • customfield_YYYYY:Story Points

やったこと自体はシンプルで、

  1. API レスポンス(fields 全体)を取得する
  2. Jira の画面表示と突き合わせて、どれが何かを確認する
  3. 必要なものだけ “基本情報” に寄せて整形する

という流れです。 ただ、項目数が多いので、途中は AI に「画面表示と対応が取れそうなフィールド名の候補」を拾ってもらい、最後は人間が画面で確定させました (この辺は MCP の話というより「Jira 特有のデータ構造」に起因する話ですね)。

使用例(会話ログ):こういう時に便利だった

※以下は説明のための会話例です(プロジェクト名・チケットキー等はダミーです)。

1) 自分の持ちタスクを確認する(my_active_issues

:「今 PR 出した。次なにやればいい? 自分の未完了タスクをざっと見せて」
Copilot:(my_active_issues を実行)
Copilot:「未完了が 20 件あります。進行中は 6 件、未着手は 14 件です。直近更新順に並べると…」

バックログを開かずに “持ち玉” を把握できるので、PR を出したあとに次のタスクを探す時に便利でした。

2) なんとなくで探す(search_issues

:「検索基盤まわりのタスクって何が残ってた?」
Copilot:(search_issues を実行。内部ではタイトル・説明文に引っかける検索)
Copilot:「関連しそうな課題がいくつかあります。直近更新順で…」

厳密な検索というより「思い出し用途」に寄りますが、雑に聞けるのは良かったです。
(JQL を自分で書かなくて済むのが嬉しい)

3) 今のブランチでやることを確認する(get_issue

:「数日空いちゃった。このブランチ、何の対応だっけ?」
Copilot:(ブランチ名から PROJECT-1234 を抽出 → get_issue を実行)
Copilot:「このタスクの要点は… 次のステップは…」

ブランチ命名規則とセットですが、「チケット番号を打たなくていい」のは地味に効きました。

ここからが難しかった:PoC の次は「運用」の話になる

ツール自体は動きました。ドキュメントも書きました。
でも「チームで使う」となると、そこから先は実装とは違う種類の難しさが出てきます。

便利さの話というより、「これを誰が面倒見続けるのか」「困ったとき誰に聞けばいいのか」みたいな、運用の話です。

特に社内利用だと、ざっくり以下のような論点が出てきます(個別のプロセスはぼかします)。

  • 利用目的:何のために使うのか、業務改善として説明できるか
  • データの扱い:どんな情報が外に出る可能性があるか、ログに残らないか
  • 依存先の信頼性:サードパーティやコミュニティ実装をどう評価するか(供給網リスク含む)
  • 認証・権限:認証方式(PAT / OAuth 等)をどうするか、トークンやクライアントシークレット等の認証情報を誰がどこで管理し、期限切れや漏洩時にどうローテーション/失効させるか(=運用ルールを作る必要がある)
  • 問い合わせ先・保守:困ったとき誰が面倒を見るか、更新は誰が追うか

結局のところ、個人 PoC を “便利ツール” から “チームの道具” に昇格させるには、

  • 問い合わせを受ける
  • 依存関係を更新し続ける
  • VSCode / Copilot / Jira の更新に追従して動作保証する

というコストが避けられません。

「便利さは分かるけど、チームの道具として採用するには材料が足りない」という状態になりました。

具体的には、

  • (1) 誰が問い合わせ窓口になるのか
  • (2) アップデート追従や障害対応をどこまで面倒見るのか
  • (3) 認証情報(PAT)をどう配り・保管し・失効させるのか

――といった運用設計を詰める必要がありました。

一方で私は、その設計や合意形成まで含めて前に進める時間と覚悟を用意できず、結果的に「PoC で止める」判断をしました。

決定打:GitHub 側の自動化レールが強かった

もう一つ、状況を決めたのは GitHub 側の流れです。

GitHub だと、Issue を起点にして Copilot(Coding Agent 含む)にタスクを投げ、PR までつなげる導線がどんどん強くなっています。

この場合、情報の置き場が GitHub 側に寄っている方が “流れ” が自然です。

  • Jira を起点にして情報を吸う → GitHub 側に落とし直す → 実装・PR

よりも

  • 最初から GitHub Issue に “実装に必要な単位” で書いて、Copilot に投げる

方が、流れとして強い

結果として、実装タスクは GitHub 側に寄せる場面が増えていきました。
(もちろん、プロセス上 Jira が必要な領域は残るので、完全に置き換えられるわけではありません)

これがタイトルの「ツールの引力」です。
個々のツールの便利さというより、「どこに情報を書くと開発フローの摩擦が一番小さいか」に引っ張られました。

ネットワークの話:ローカルは動く。でも “拡張” は別問題

前提として、ローカル利用とクラウド実行では難しさが変わります。

  • ローカル(VSCode 内)で使う MCP サーバーとしては、社内ネットワークに到達できるので問題なし
  • 問題になるのは、GitHub Actions など クラウド実行(Coding Agent を含む)に寄せようとした瞬間

クラウドからオンプレの Jira に触るには、到達経路(Self-hosted runner 等)を含む設計が必要になります。
できなくはないですが、セキュリティ・運用の論点が一気に増えます。

そこまで踏み込むほどのリターンを見出せませんでした。

結論:動いた。でも、運用に乗せるところで力尽きた

Jira を参照できる MCP サーバーは、PoC としては狙いどおり動きました。 ブラウザを開く回数が減って、集中が切れにくくなる感覚も確かにありました。

一方で、ここから「チームの道具」にする段階で、私には運用まで推し進める力が足りませんでした。

  • 認証情報(PAT)をどう扱うのが安全か
  • 依存ライブラリや VSCode / Copilot / Jira の更新に追従して動作保証できるか
  • 困ったときの問い合わせ先を誰にするか(=結局、自分が面倒を見るのか)

こういう話が避けられないのは分かっていたのですが、当時の自分には「そこまで背負って広げる」だけの決定打を用意できませんでした。 結果として、便利さは感じつつも運用に乗せる前に止める判断になりました。

もうひとつ、気持ちが傾いた理由として Copilot を使った開発の導線が GitHub 側でどんどん強くなっていったこともあります。 実装タスクが GitHub に寄るほど、Jira を読むための仕組みを増やすより、最初から GitHub 側で完結させた方が流れとして自然でした。

今回の話は「MCP サーバーを作るのは難しい」というより、 作ったものを“使われ続ける道具”にするのが難しい 、という反省でした。 これはツールや組織の良し悪しというより、PoC と運用の要求水準のギャップが埋めきれなかった、という話です。

参考リンク


バックエンドエンジニアです。辛いものが好き、と言う割にいつも“ちょい辛”あたりに落ち着く慎重派。