every Tech Blog

株式会社エブリーのTech Blogです。

Makefile を MCP サーバー化してちょっとだけ開発効率を上げる

タイトル「Makefile を MCP サーバー化してちょっとだけ開発効率を上げる」

はじめに

株式会社エブリーでCTOをしている imakei です。
本日から弊社では多くの新卒メンバーに入社していただきました。
これから彼ら・彼女らとともにより強い開発組織を作っていきたいと思います。

すでに新卒メンバーからは学ぶことも多く、特にAIに関しては自分以上に使いこなしているところを数多くみています。 そんな彼らに感化された部分もあり、生成AIを使いながら開発していく上でのちょっとしたTipsを紹介できればと思います。

弊社では今積極的に生成AIを活用した開発を行っています。
AIAgentを利用した開発も積極的に試しているのですが、そんな中で、
なかなか思っているコマンドを実行してくれないことにストレスを感じていたので、
個人的にModel Context Protocol (MCP) の理解も兼ねてちょっとしたツールを開発してみたので紹介できればと思います。

※あくまで個人のローカルでの開発を想定したツールです。パブリックに公開するなどは一切考えていないので、その辺はご了承ください。

今回はMakefileを元にMCPサーバーを作成して、それをAIAgentに利用してみたいと思います。

MakefileをMCPサーバー化して開発効率を上げる方法

はじめに

開発プロジェクトでMakefileを使用している方も多いのではないでしょうか。ビルド、テスト、デプロイなど、様々なタスクを効率的に実行できる便利なツールですが、
せっかく整備していてもAgentがそれをうまく使ってくれないということが度々あります。

MCPとは

Model Context Protocol (MCP) は、AIアシスタントがローカル環境のツールを実行するためのプロトコルです。これを利用することで、自然言語でMakefileのターゲットを実行できるようになります。

例えば:
「テストを実行して」
「開発サーバーを起動して」
といった指示をAIアシスタントに伝えるだけで、対応するmakeコマンドを実行できます。

実装方法

必要な環境

  • Deno // 自分は雑なツールを作るときにDenoが好きで使っていますが、正直なんでも良いと思います。
  • MCP SDK

コアとなる実装

MCPサーバーの実装

まず、MCPサーバーの実装です。
SDKが提供されているのでそれほど難しくはなく立ち上げられると思います。

script.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// MCPサーバーのセットアップ
const server = new Server(
  {
    name: "local_makefile_mcp",
    version: "0.0.1",
  },
  {
    capabilities: {
      resources: {},
      tools: {
        ...makeTools,  // Makefileのターゲットから生成したツール群
      },
    },
  }
);
await server.connect(new StdioServerTransport());

ツールの生成

Makefileを読み込んで、その中にあるターゲットをツールとして登録しています。
自分用に作ったので、matchesで愚直にやっていますが、より良い方法はありそうです。

script.ts

function getMakeCommands(makefilePath?: string): Promise<string[]> {
  const cmd = ["make", "-n"];
  if (makefilePath) {
    cmd.push("-f", makefilePath);
  }

  // Makefileの内容を直接読み取る
  const makefileContent = makefilePath
    ? await Deno.readTextFile(makefilePath)
    : await Deno.readTextFile("Makefile");

  // ターゲットを抽出
  const matches = [...makefileContent.matchAll(/^([a-zA-Z0-9_\-\.]+):/gm)];
  const targets = matches.map(m => m[1]);

  return [...new Set(targets)].filter(target =>
    !target.startsWith('.') &&
    !['Makefile', 'makefile', 'GNUmakefile', 'DEFAULT', 'SUFFIXES'].includes(target)
  );
}

function generateMakeTools(targets: string[]): Record<string, Tool> {
  const makeTools: Record<string, Tool> = {};
  for (const target of targets) {
    makeTools[`make_${target}`] = {
      name: `make_${target}`,
      description: `Run 'make ${target}'`,
      inputSchema: {
        type: "object",
        properties: {},
        required: [],
      },
    };
  }
  return makeTools;
}

// makefileから取得したcommandsを `make_<command>` という名前でツールとして登録している
const commands = getMakeCommands('path/to/Makefile');
const makeTools = generateMakeTools(commands);

ツールの実行ハンドラー

今回はmakefileから取得したcommandsを make_<command> という名前でツールとして登録しているので、
それを実行するハンドラーを作成します。

script.ts

// ツールの実行ハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
  const name = request.params.name;

  if (name.startsWith("make_")) {
    const target = name.replace("make_", "");
    const cmd = ["make"];
    if (makefilePath) {
      cmd.push("-f", makefilePath);
    }
    cmd.push(target);

    // Makefileのディレクトリパスを取得
    const makefileDir = new URL('.', 'file://' + makefilePath).pathname;

    const command = new Deno.Command("make", {
      args: cmd.slice(1), // "make"を除いた引数を渡す
      cwd: makefileDir,
      stdout: "piped",
      stderr: "piped",
    });

    const { stdout, stderr } = await command.output();
    
    const output = new TextDecoder().decode(stdout);
    const error = new TextDecoder().decode(stderr);

    // ...

    return {
      content: [
        {
          type: "text",
          text: output,
        },
      ],
      isError: false,
    };
  }

  // 知らないコマンドきた時のハンドリング

});

このコアとなる実装では、以下の重要な機能を提供しています:

  1. MCPサーバーの初期化

    • サーバー名とバージョンの設定
    • 利用可能なツール(makeターゲット)の登録
  2. Makefileターゲットの解析

    • Makefileの内容を読み取り
    • 有効なターゲットの抽出
    • 特殊ターゲットのフィルタリング
  3. ツール定義の生成

    • 各makeターゲットに対応するツールの定義
    • ツール名とコマンドのマッピング
    • ツール説明の生成
  4. ツール実行ハンドラー

    • makeコマンドの実行
    • 標準出力とエラー出力の取得
    • 実行結果のフォーマット化

これにより、AIアシスタントはMakefileのターゲットを安全に実行し、その結果を適切にユーザーに返すことができます。

使用方法(Clineでの利用)

ClineのパネルのMCPタブを開き、Configure MCP Serversを押下すると、
cline_mcp_settings.json というファイルが開くので設定していきます。

cline_mcp_settings.json

{
  "mcpServers": {
    "local_makefile_mcp": {
      "command": "path/to/deno/command",
      "args": ["run", "-A", "path/to/script.ts", "path/to/Makefile"],
      ...
    }
  }
}

これで MCP Servers に local という名前でサーバーが追加されます。

local_makefile_mcpが読み込まれている様子

MCPサーバーを起動後、AIアシスタントに実際にTestをお願いすると、mcpサーバーからコマンドを見つけてくれます。

mcpサーバーからテストコマンドを見つけてくれた様子

無事にコマンドを見つけられていますね。
ただ正直このままですと、自然言語でコマンドを見つけてくれないこともあるので、
Makefileにコメント等をつけてそれをディスクリプションに登録するといいかもしれません。

まとめ

このように、MCPサーバーを利用することで、自然言語でMakefileのターゲットを実行できるようになります。 細かい部分にはなりますが、これにより開発におけるAgent利用のストレスが少し減ったように思います。

今後もこういった細かいことから生成AIをもっとストレスなく活用して開発効率を上げていきたいと思っています。
そんな弊社は現在積極採用中です!

こんな開発を普段から行なっている弊社で働きたいなと思った方はぜひ一度お話ししましょう!

https://twitter.com/imakei_
https://corp.every.tv/recruits/engineer

今井