「プロンプトに足す」では届かない“安全な実行”を——公式 MCP+社内コマンドMCP化で実現する

こんにちは!YOUTRUSTのアプリエンジニアの葉(YOUTRUST)です!

今回は、最近話題の MCP(Model Context Protocol) を使って、AI にコマンドを“安全に任せる”方法を試してみたので、シェアしたいと思います!

🚀なぜ MCP なのか

最近、AI を開発に使う中で、ちょっとしたノイズを感じることがありました。
同じ依頼でも毎回出力が微妙に違う(設定が抜けたり、フォーマットがずれる)。

人間との会話のように柔軟で便利なんですが、ちょっとした言い回しの違いで結果が変わったり、毎回の動作がブレたりするのが悩みでした。
「言葉」で伝える限界をどう補うかを考えたとき、 AI に実行を任せるためのルールをコードとして定義するのが一番早いと気づきました。

そこで方針をシンプルに:

① 公式の「Dart & Flutter MCP Server」を使う
② 足りない“社内コマンド”は自前MCPツール化して、安全・再現・構造化を担保する

この組み合わせで、プロンプトだけでは届かない “安全で再現性のある実行” が可能になります。

自分は普段 Flutter 開発が多いので Dart / Flutter MCP を使いましたが、実はほかにも Python や Node.js、Rust など、さまざまな公式 MCP サーバーがあります。
自分の環境に合うものを探してみるのもおすすめです!🔍

🧠MCP とは(超要約)

MCP(Model Context Protocol)は、LLMとツールをつなぐための標準プロトコルです。

サーバー側が「使ってよい Tools(機能)」「読める Resources」「定型 プロンプト」などを型付き(JSON Schema)で公開し、クライアント(Claude / Cursor / Gemini CLI…)がtools/list → tools/call で発見・実行できるようになります。

⚙️なぜ「プロンプト追記」だけでは足りないのか(コマンド限定の観点)

観点 プロンプトに追記(CLAUDE.md/コマンド等) MCP(型付きツール)
指示 人が読む文章(解釈ゆれ) subcommand/args をスキーマで固定
安全 文章の約束 ホワイトリスト+バリデーション+タイムアウト
出力 都度違うログ 毎回 {exitCode, stdout, stderr}
再実行 言い方・担当者依存 機械的に同じ(tools/call)
再利用 クライアント依存 複数クライアント横断で再利用

つまり、「再現性のない CLI 操作」をツールとして再定義する感じです。

🧩このように運用してみた

1. 公式 MCPを使う!

Flutter 開発での基本操作(analyze/test/format など)はすでに Dart and Flutter MCP server が全部やってくれます。
“AI にコマンドを任せる” なら、まずこれから始めるのが一番おすすめです。

ちなみに、自分の環境では ~/.cursor/mcp.json では反応せず、 youtrust-app/.cursor/mcp.json に置いたらうまく動きました。

❌ ~/.cursor/mcp.json  
✅ youtrust-app/.cursor/mcp.json

動くとこんな感じになります👇

dart format をちゃんと叩いてくれました!🎉

2. 自前で MCP サーバーを作る!

🪄 1. まずは公式チュートリアルを動かす

公式の Build an MCP server チュートリアルは、Python + FastMCP で “天気API” を公開する最小構成です。

  1. uv(Astral製の高速パッケージマネージャ) で環境構築
  2. API 呼び出しを関数化(Helper)
  3. @mcp.tool() でツール定義
  4. mcp.run(transport='stdio') で起動

これで MCP の仕組みがサクッと掴めます。

👇 実際に動かすとこんな感じ。

🔧 2. 公式チュートリアルから“自分仕様”へ

YOUTRUST では make build_runner.youtrust.run をよく使うので、 これを MCP サーバー化してみました。

Claude Code にお願いして作ったのがこれ👇

# /// script
# requires-python = ">=3.13"
# dependencies = [
#   "mcp[cli]>=1.16.0",
# ]
# ///

import asyncio
import json
import subprocess
import os
from pathlib import Path
from typing import Any, Optional
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("flutter_dev")

# Global dictionary to track running Flutter processes
running_processes: dict[str, subprocess.Popen] = {}

async def run_command(cmd: list[str], timeout: int = 30, cwd: Optional[str] = None) -> dict[str, Any]:
    """Run a command and return the result as a dictionary."""
    try:
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=cwd
        )

        stdout, stderr = await asyncio.wait_for(
            process.communicate(),
            timeout=timeout
        )

        return {
            "success": process.returncode == 0,
            "returncode": process.returncode,
            "stdout": stdout.decode('utf-8', errors='ignore'),
            "stderr": stderr.decode('utf-8', errors='ignore')
        }
    except asyncio.TimeoutError:
        return {
            "success": False,
            "error": f"Command timed out after {timeout} seconds"
        }
    except Exception as e:
        return {
            "success": False,
            "error": str(e)
        }


@mcp.tool()
async def run_build_runner(
    timeout: int = 300
) -> str:
    """Run build_runner for code generation using make build_runner.youtrust.run.
    
    Args:
        timeout: Command timeout in seconds (default: 300)
    
    Returns JSON with command output.
    """
    cmd = ["make", "build_runner.youtrust.run"]
    
    try:
        result = await run_command(cmd, timeout=timeout)
    except RuntimeError as e:
        return json.dumps({
            "success": False,
            "error": str(e)
        })
    
    return json.dumps({
        "success": result["success"],
        "command": " ".join(cmd),
        "returncode": result.get("returncode"),
        "stdout": result["stdout"],
        "stderr": result["stderr"],
        "error": result.get("error")
    }, ensure_ascii=False, indent=2)


def main():
    """Initialize and run the MCP server."""
    mcp.run(transport='stdio')


if __name__ == "__main__":
    main()

起動用 alias(~/.zshrc)

alias flutter-dev="uv run weather/flutter_dev.py"

youtrust-app/.cursor/mcp.json に追加

{
    "mcpServers": {
        "flutter-dev": {
            "command": "zsh",
            "args": [
                "-ic",
                "flutter-dev"
            ]
        }
    }
}

🚨実装時のハマりどころ

公式チュートリアルでは directory を MCP サーバー(スクリプト側)に設定していますが、 自分の環境ではそれだと make コマンドが動きませんでした。

make はプロジェクトディレクトリ内でしか動作しないため、 youtrust-app/.cursor/mcp.json に alias で設定したコマンドを走らせるようにしたら、 ちゃんと拾ってくれるようになりました ✅

これで、Cursor から MCP サーバーとして make build_runner.youtrust.run を安全に叩けるようになります 🎉

「mcpで make build して」って言うだけで動くの、ちょっと感動します。

💡おわり

今回は MCP(Model Context Protocol)について紹介しました。
今後は、社内のコマンドや情報をどんどん MCP 化していき、 安全・再現性・構造化 を実現していきたいと思います。

AI の力で “安全な自動化” を進めたい方は、ぜひ自分の環境でも試してみてください!

自分も今回が初挑戦なので、「ここ違うかも」「こっちの方が便利だよ」みたいな気づきがあったら、ぜひコメントで教えてもらえると嬉しいです!🙌

herp.careers