Claude Codeを社内ツールにつなぐ:MCPで社内システムと連携する

MCPで社内システムをClaude Codeに接続する方法を解説。汎用fetchから専用Server構築まで、デプロイ基盤・監視・チケット管理などの接続パターンとツール設計の原則を網羅。


前回はデータベースとの接続について解説した。データベースには標準プロトコルがあるので、接続は比較的シンプルだ。しかし、多くのチームの日常業務はさまざまな社内システムに依存している:デプロイ基盤、監視ダッシュボード、チケット管理、社内API、設定管理など。

これらのシステムには既成のMCP Serverがないことがほとんどだが、ほぼすべてHTTP APIを提供している。この記事では、MCPを使ってこれらの社内ツールをClaude Codeに接続し、監視の確認やデプロイ状況の把握、チケット操作を直接行えるようにする方法を解説する。

2つのアプローチ

社内ツールの接続には2つの方法がある:

方法1:汎用HTTP MCP Serverを使う
コミュニティには汎用的なMCP Serverがいくつかあり、任意のREST APIをMCPツールとしてラップできる。API定義ファイルを書けば、Claudeが呼び出せるツールに変換してくれる。APIの構造がシンプルで、複雑なロジックが不要な場合に適している。

方法2:専用のMCP Serverを自作する
TypeScriptまたはPythonのMCP SDKを使って専用Serverを書き、ツールの定義、パラメータのバリデーション、エラーハンドリングを完全にコントロールする。複数のAPIを組み合わせたり、データ変換やビジネスロジックを加えたりする場合に適している。

この記事では両方を解説する。まずはシンプルな方から始めよう。

方法1:mcp-server-fetchで手軽に接続する

最も軽量なのは、公式の @anthropic-ai/mcp-server-fetch を使う方法だ。Claudeが直接HTTPリクエストを送れるようになる。設定は極めてシンプル:

{
  "mcpServers": {
    "fetch": {
      "command": "npx",
      "args": ["-y", "@anthropic-ai/mcp-server-fetch"]
    }
  }
}

設定が済めば、Claudeから直接社内APIを呼び出せる:

デプロイ基盤 https://deploy.internal.com/api/v1/services/user-service の現在の状態を確認して

ClaudeがGETリクエストを送り、レスポンスを取得してパースしてくれる。

ただし、この方法には明らかな制限がある:

  • 毎回Claudeに完全なURLとリクエスト形式を伝える必要がある
  • パラメータのバリデーションがなく、Claudeがパスを間違える可能性がある
  • 認証情報を毎回渡すか、プロンプトに書く必要がある(安全でない)
  • 複数のAPI呼び出しを組み合わせられない

一時的な用途には向いているが、長期的な運用には適さない。

方法2:専用MCP Serverを書く

特定の社内システムを繰り返し使う場合は、専用のMCP Serverを書くほうがよい。ここでは実際のユースケースとして、社内デプロイ基盤への接続をデモする。

デプロイ基盤が以下のAPIを提供していると仮定する:

  • GET /api/v1/services — すべてのサービスを一覧表示
  • GET /api/v1/services/:name/status — サービスの状態を確認
  • POST /api/v1/services/:name/deploy — デプロイをトリガー
  • GET /api/v1/services/:name/logs — 最近のデプロイログを表示

TypeScriptでMCP Serverを書く

まずプロジェクトを初期化する:

mkdir mcp-deploy && cd mcp-deploy
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init

コアとなるコード src/index.ts

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

const API_BASE = process.env.DEPLOY_API_URL!;
const API_TOKEN = process.env.DEPLOY_API_TOKEN!;

async function api(path: string, method = "GET", body?: unknown) {
  const res = await fetch(`${API_BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    throw new Error(`API error: ${res.status} ${await res.text()}`);
  }
  return res.json();
}

const server = new McpServer({
  name: "deploy-platform",
  version: "1.0.0",
});

// すべてのサービスを一覧表示
server.tool("list_services", "デプロイ基盤上のすべてのサービスとその状態を一覧表示する", {}, async () => {
  const data = await api("/api/v1/services");
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
});

// 個別サービスの状態を確認
server.tool(
  "service_status",
  "指定されたサービスの現在のデプロイ状態、バージョン、ヘルスチェック結果を確認する",
  { name: z.string().describe("サービス名(例:user-service)") },
  async ({ name }) => {
    const data = await api(`/api/v1/services/${name}/status`);
    return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
  }
);

// デプロイログを確認
server.tool(
  "deploy_logs",
  "指定されたサービスの最近のデプロイログを表示する",
  {
    name: z.string().describe("サービス名"),
    limit: z.number().optional().default(10).describe("取得件数(デフォルト:10)"),
  },
  async ({ name, limit }) => {
    const data = await api(`/api/v1/services/${name}/logs?limit=${limit}`);
    return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
  }
);

// デプロイをトリガー
server.tool(
  "trigger_deploy",
  "指定されたサービスのデプロイをトリガーする。これは書き込み操作であり、本番環境に影響を与える",
  {
    name: z.string().describe("サービス名"),
    version: z.string().describe("デプロイするバージョン番号またはgit ref"),
  },
  async ({ name, version }) => {
    const data = await api(`/api/v1/services/${name}/deploy`, "POST", {
      version,
    });
    return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
  }
);

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

コンパイルして実行:

npx tsc

Claude Codeの設定

{
  "mcpServers": {
    "deploy": {
      "command": "node",
      "args": ["/path/to/mcp-deploy/dist/index.js"],
      "env": {
        "DEPLOY_API_URL": "https://deploy.internal.com",
        "DEPLOY_API_TOKEN": "your-api-token"
      }
    }
  }
}

tokenは .claude/settings.local.json(gitにコミットしない)に、URLは .claude/settings.json(gitにコミットしてチームで共有)に配置する。

使用イメージ

設定が完了すると、会話が自然になる:

user-serviceの現在の状態は?

→ Claudeが service_status("user-service") を呼び出す
→ 結果:稼働中、バージョンv2.3.1、最終デプロイは2時間前、ヘルスチェックはすべてパス
最近のデプロイで失敗したものはある?

→ Claudeが deploy_logs("user-service", 20) を呼び出す
→ ログを分析し、3回目のデプロイがロールバックされたこと、原因はヘルスチェックのタイムアウトだと報告
user-serviceをv2.3.2にデプロイして

→ Claudeが trigger_deploy("user-service", "v2.3.2") を呼び出す
→ ツールの説明に「書き込み操作」と記載されているため、Claudeはまず確認を求める

書き込み操作は接続すべきか

これは慎重に検討すべき問題だ。

読み取り操作は安心して接続できる。状態の確認、ログの閲覧、チケットの検索——これらは副作用がなく、Claudeが間違えても損害はない。

書き込み操作は2つのケースに分かれる

低リスクな書き込み操作は接続してよいが、ツールの説明に明記すること。Claudeは副作用が明記された操作に対して自動的にユーザーの確認を求める。例えばチケットの作成、メッセージの送信、設定の更新など。

高リスクな書き込み操作は接続しないことを推奨する。リソースの削除、ロールバックのトリガー、権限の変更——これらは結果が深刻で不可逆なため、手動操作のままにしておくほうが安全だ。

どうしても書き込み操作を接続する場合は、少なくとも2つのことを行う:

  1. ツールの説明に「これは書き込み操作であり、本番環境に影響を与える」と明記する
  2. MCP Server内に必要なセーフガードを追加する(例えば、productionネームスペースへのデプロイを禁止するなど)

よくある社内システムの接続パターン

システム 公開するツール 注意点
デプロイ基盤(K8s / Kamal) サービス状態の確認、ログの表示、デプロイのトリガー 書き込み操作には確認を追加
監視システム(Grafana / Datadog) メトリクスの表示、アラート履歴の確認 クエリの時間範囲を制限し、大量のデータ取得を避ける
チケット管理(Jira / Linear) チケットの検索、作成、ステータスの更新 チケット作成は書き込み操作だが低リスク
社内ドキュメント(Notion / Confluence) ドキュメントの検索、ページ内容の読み取り ページネーションに注意し、一度に大量に取得しない
設定管理(Consul / etcd) 設定の読み取り、環境間の差分比較 読み取りのみにし、書き込み操作は接続しない
CI/CD(GitHub Actions / Jenkins) ビルド状態の確認、ビルドのトリガー ビルドのトリガーは中リスクの書き込み操作

ツール設計の原則

MCPツールの設計はAPI設計とは異なる。APIはエンジニアが使うものだが、ツールはAIが使うものだ。いくつかの原則を押さえておきたい:

ツール名は分かりやすく

✗ get_svc_stat     — Claudeが略語の意味を正しく推測できるとは限らない
✓ service_status   — 見ればすぐに何をするか分かる

説明はAI向けに書く

ツールの説明は人間向けのドキュメントではなく、Claudeが「いつこのツールを呼び出すべきか」を判断するための根拠だ。説明には、このツールが何をするか、何を返すか、いつ使うべきかを明記する。

✗ "サービス状態を取得する"
✓ "指定されたサービスの現在のデプロイ状態、バージョン、ヘルスチェック結果を確認する。ユーザーがあるサービスが正常に稼働しているか質問した場合に使用する"

パラメータはzodで明確に定義する

.describe() を付けたパラメータだけが、Claudeに何を入力すべきかを伝えられる。説明のないパラメータは、名前から推測するしかない。

構造化されたデータを返す

MCPツールが返すのはテキストだが、できるだけフォーマットされたJSONを返すようにする。Claudeは構造化されたデータのほうがプレーンテキストよりも正確に処理できる。

適切な粒度を保つ

複雑なフローを1つのツールに詰め込まないこと。逆に、シンプルなクエリを3つのツールに分割するのもよくない。原則は、1つのツールで1つの独立した意味のある操作を完結させることだ。

MCP Serverの配置場所

MCP Serverのコードはいくつかの場所に配置できる:

プロジェクトリポジトリ内(推奨のスタート方法)

your-project/
├── .claude/settings.json
├── mcp-servers/
│   └── deploy/
│       ├── src/index.ts
│       ├── package.json
│       └── tsconfig.json
└── ...

コードと設定が一箇所にまとまるため、チームがcloneして依存関係をインストールすればすぐに使える。

独立リポジトリ

MCP Serverを複数のプロジェクトで使う場合は、独立したリポジトリに配置し、npmパッケージやDockerイメージとして公開する。

{
  "mcpServers": {
    "deploy": {
      "command": "npx",
      "args": ["-y", "@yourcompany/mcp-deploy-server"]
    }
  }
}

グローバルインストール

全社共通のMCP Server(例えば、統一認証や統一ログ基盤との接続)は、グローバルにインストールして ~/.claude/settings.json に設定する。

デバッグのコツ

MCP Server開発でよくある問題は「Claudeがツールを呼び出さない」や「呼び出したがエラーになる」だ。

Serverが起動していることを確認する

Claude Codeを再起動した後、/mcp と入力して接続済みのMCP Server一覧を確認する。自分のServerが一覧にない場合は、commandとargsが正しいか確認する。

Serverを単独でテストする

MCP Serverはstdioで通信するので、ターミナルから直接テストできる:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

ツール一覧が返ってくれば、Server自体は正常だ。

Claudeのツール呼び出しログを確認する

Claude Codeは毎回のツール呼び出しの入力と出力を表示する。パラメータが間違っている場合は、たいていツールの説明やパラメータ定義が不十分で、Claudeが誤解したことが原因だ。

実践例:Sentryエラートラッキングの接続

実際のユースケースでまとめてみよう。SentryをClaude Codeに接続し、本番のエラーを直接確認できるようにする。

server.tool(
  "search_errors",
  "在 Sentry 中搜索最近的錯誤。用於排查線上問題、查看錯誤趨勢",
  {
    query: z.string().describe("搜索關鍵詞,如錯誤訊息、函式名"),
    hours: z.number().optional().default(24).describe("查看最近多少小時的錯誤"),
  },
  async ({ query, hours }) => {
    const since = new Date(Date.now() - hours * 3600000).toISOString();
    const data = await api(
      `/api/0/projects/${ORG}/${PROJECT}/issues/?query=${encodeURIComponent(query)}&start=${since}&sort=date`
    );
    const summary = data.map((issue: any) => ({
      title: issue.title,
      count: issue.count,
      firstSeen: issue.firstSeen,
      lastSeen: issue.lastSeen,
      link: issue.permalink,
    }));
    return {
      content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
    };
  }
);

server.tool(
  "error_details",
  "查看 Sentry 中某個錯誤的詳細資訊,包括堆疊和最近一次事件",
  { issueId: z.string().describe("Sentry issue ID") },
  async ({ issueId }) => {
    const [issue, latest] = await Promise.all([
      api(`/api/0/issues/${issueId}/`),
      api(`/api/0/issues/${issueId}/events/latest/`),
    ]);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              title: issue.title,
              count: issue.count,
              users: issue.userCount,
              stacktrace: latest.entries?.find(
                (e: any) => e.type === "exception"
              ),
            },
            null,
            2
          ),
        },
      ],
    };
  }
);

接続が完了すると、本番の問題調査はこんな会話になる:

直近4時間で新しい500エラーは出ている?

→ ClaudeがSentryを検索
→ 3件の新しいissueを発見、最も深刻なものは120人のユーザーに影響
→ 自動的にスタックトレースを取得し、あるNull Pointer Exceptionが原因と特定
→ コード内の該当箇所を見つけ、修正案を提示

問題の発見からコードの特定まで、すべて1回の会話で完結する。

次のステップ

この記事では、MCPを使って社内ツールを接続する方法を解説した。核心となる考え方は、社内システムにHTTP APIがある → MCP Serverでラップする → Claudeが直接使えるようになる、というものだ。

この記事のすべての例は、既存のAPIのラッピングだ。デプロイ基盤もSentryも元々インターフェースを持っており、MCP Serverは転送と適応のレイヤーにすぎない。次回は異なるシナリオを扱う:必要な機能に既成のAPIがない場合に、ゼロからMCP Serverを構築し、自前でロジックを実装し、状態を管理し、複雑なマルチステップのインタラクションを処理する方法だ。