🪜

FFmpeg の progress を可視化する MCP ツール

に公開

前回まで

前回は Stream 配信のソースとなる FFmpeg のスタッツを GraphQL で観測できるところまで仕込みました。

今回は

GraphQL のところを MCP ツール化することで AI の力を借り、より楽に観測できるようにしたと思います。

具体的には

🧠 Claude で 🛠️ MCP を利用し、下記の 2 つを実現するシナリオを考えます。

  1. 独立した二つの GraphQL データの相関分析
    📈 FFmpeg の観測データ
    📉 Cloudflare Stream の観測データ

  2. GraphQL で手動で行った分析作業を自動化一発
    ⚠️ FFmpeg の異常値がある場所を探し
    📊 そこを含む時間帯の、他のスタッツも採取し、状況を俯瞰

ピンク部分が今回の範囲。

FFmpeg 観測 MCP サーバー(FFmpeg GraphQL に被せる MCP ツール)

FFmpeg の GraphQL 分析データを Claude から探れる MCP サーバーをたてます。
MCP サーバーから D1 直叩きでもいいかもですが、GraphQL 噛ませたんで、こっちでも使います。

JSON-RPC とストリーミング(SSE)の 2 つの HTTP パスを用意しました。
最初 hono/mcp を使って SSE を作ったけど、Claude はうまく行かず JSON を追加、という流れでした。

また MCP ツールは GrapqhQL クエリーにそのまま個別に被せることにしました。
(クエリーを組み合わせてツールを作ったり、色々できそうではありましたが)

ルーター
mcp/index.ts
import { Hono } from 'hono'
import { mcpJson } from './json'
import { mcpStream } from './stream'

const app = new Hono()

app.route("/", mcpJson)
app.route("/stream", mcpStream)

export const mcp = app
ファイル
tree
├── mcp
│   ├── core.ts
│   ├── index.ts
│   ├── json.ts  --> application/json 用
│   └── stream
│       ├── http.ts
│       └── index.ts --> text/event-stream 用

共通で使うGraphQL のクエリ関数。
latestMetrics の例。他の GraphQL 用も同様に作ります。

mcp/core.ts

export async function analyzeLatestMetrics(stream_id?: string) {
  const gqlUrl = (env as any).GQL_URL
  const hasStreamId = stream_id && stream_id.trim() !== ""
  const response = await fetch(gqlUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query: hasStreamId
        ? `query FindLag($id: String!) {
             latestMetrics(stream_id: $id) {
               ts
               bucket_time_iso_jst
               fps
               speed
               bitrate_kbps
               q
               drop_frms
               dup_frms
             }
           }`
        : `query FindLag {
             latestMetrics {
               ts
               bucket_time_iso_jst
               fps
               speed
               bitrate_kbps
               q
               drop_frms
               dup_frms
             }
           }`,
      variables: hasStreamId ? { id: stream_id } : {}
    })
  })

  if (!response.ok) {
    throw new Error(await response.text())
  }

  return await response.json()
}                              

Claude に見せる JSON-RPC エンドポイント。
愚直に各メソッド対応 initialize - tools/list - tools/call

mcp/json.ts
import { Hono } from 'hono'
import { analyzeLatestMetrics } from './core'

const app = new Hono()

app.post('/', async (c) => {
  const payload = await c.req.json()

  const { method, id, params } = payload

  /* =========================
   * initialize
   * ========================= */
  if (method === 'initialize') {
    return c.json({
      jsonrpc: '2.0',
      id,
      result: {
        protocolVersion: params?.protocolVersion ?? '2025-11-25',
        capabilities: {
          tools: {}
        },
        serverInfo: {
          name: 'ffmpeg-analyst',
          version: '1.0.0'
        }
      }
    })
  }

  /* =========================
   * tools/list
   * ========================= */
  if (method === 'tools/list') {
    return c.json({
      jsonrpc: '2.0',
      id,
      result: {
        tools: [
          {
            name: 'analyze_latest_metrics',
            description: 'FFmpeg 直近情報の分析 (GraphQL経由):このツールはFFmpegの直近の進捗を監視するためのもので、配信の状況把握、遅延や画質低下の検知や原因分析に使用してください。',
            inputSchema: {
              type: 'object',
              properties: {
                stream_id: {
                  type: 'string',
                  description: '空欄で可。入力を促す必要なない。デフォルトIDを自動補完。分析対象のストリームID'
                }
              },
              required: []
            }
          }
        ]
      }
    })
  }

  /* =========================
   * tools/call
   * ========================= */
  if (method === 'tools/call') {
    const { name, arguments: args } = params

    if (name === 'analyze_latest_metrics') {
      try {
        const result = await analyzeLatestMetrics(args?.stream_id)
        console.log("analyzeLatestMetrics result:", result);
        return c.json({
          jsonrpc: '2.0',
          id,
          result: {
            content: [
              {
                type: 'text',
                text: JSON.stringify(result, null, 2)
              }
            ]
          }
        })
      } catch (err: any) {
        return c.json({
          jsonrpc: '2.0',
          id,
          error: {
            code: -32000,
            message: err.message ?? 'Tool execution failed'
          }
        })
      }
    }

    return c.json({
      jsonrpc: '2.0',
      id,
      error: {
        code: -32602,
        message: `Unknown tool: ${name}`
      }
    })
  }

  /* =========================
   * fallback
   * ========================= */
  return c.json({
    jsonrpc: '2.0',
    id,
    error: {
      code: -32601,
      message: `Method not found: ${method}`
    }
  })
})

export const mcpJson = app                              

ストリーム(SSE)用は別ディレクトリ。
hono/mcp を利用。

mcp/stream/index.ts
import { StreamableHTTPTransport } from '@hono/mcp'
import { Hono } from 'hono'
import { createMcpServer } from './http'

const app = new Hono()

app.all('/*', async (c) => {
  const payload = await c.req.json().catch(() => ({}))
  console.log("Payload:", JSON.stringify(payload))

  const transport = new StreamableHTTPTransport()
  const mcpServer = createMcpServer()
  if (!mcpServer.isConnected()) {
    await mcpServer.connect(transport)
  }
  return transport.handleRequest(c)
})

export const mcpStream = app    
mcp/stream/http.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { z } from "zod";
import { analyzeLatestMetrics } from "../core"

export function createMcpServer() {
  const server = new McpServer({
    name: "ffmpeg-analyst",
    version: "1.0.0",
  })
  
  server.registerTool(
  "analyze_latest_metrics",
  {
    description: "FFmpeg 直近情報の分析 (GraphQL経由):このツールはFFmpegの直近の進捗を監視するためのもので、配信の状況把握、遅延や画質低下の検知や原因分析に使用してください。",
    inputSchema: {
      stream_id: z.string().describe("空欄で可。入力を促す必要なない。デフォルトIDを自動補完。分析対象のストリームID")
    }
  },    async ({ stream_id }) => {
      try {
        const data = await analyzeLatestMetrics(stream_id)

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(data, null, 2)
            }
          ]
        };
      } catch (err: any) {
        return {
          content: [
            {
              type: "text",
              text: `分析エラー: ${err.message}`
            }
          ],
          isError: true
        };
      }
    }
  );

 return server
}
export const mcpServer = createMcpServer()

MCP ツールの動作確認(MCP Inspector)

Claude に晒す前に MCP Inspector で確認しときます。
JSON・ストリーム、両方とも行けました。

/mcp(application/json)


接続 OK


レスポンス OK

packet capture (http response content-type)

application/json で応答が来ています。

/mcp
Hypertext Transfer Protocol, has 3 chunks (including last chunk)
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    Content-Type: application/json
    Access-Control-Allow-Origin: *
    Content-Encoding: gzip
    
    [Request URI: /mcp]
    HTTP chunked response
/mcp/stream(text/event-stream)


接続 OK


レスポンス OK

packet capture (http response content-type)

text/event-stream で応答が来ています。

/mcp/stream
Hypertext Transfer Protocol, has 3 chunks (including last chunk)
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    Content-Type: text/event-stream
    Access-Control-Allow-Origin: *
    Cache-Control: no-cache
    
    [Request URI: /mcp/stream]
    HTTP chunked response

Stream 観測 MCP サーバー(Cloudflare GraphQL MCP)

Cloudflare の GraphQL MCP サーバーStream にも対応しています。

Claude

mcp-remote 設定

FFmpeg は /mcp(JSON-RPC)の方を使います。
Cloudflare MCP も追加します。

claude_desktop_config.json
  "mcpServers": {
    "localtest": {
      :
      "args": ["mcp-remote", "http://localhost:8787/mcp"]
    },
    :
    "cloudflare_graphql": {
      :
      "args": ["mcp-remote", "https://graphql.mcp.cloudflare.com/mcp"],
   }
   :

シナリオの動作確認

シナリオ1

独立した二つの GraphQL データの相関分析
📈 FFmpeg の観測データ
📉 Cloudflare Stream の観測データ

FFmpeg 側

Stream 側

相関分析

グラフ化

配信ソース(FFmpeg) と 配信プラットフォーム(Stream)の状況を一枚で見れて、配信全体の見通しが良くなりました。

シナリオ 2

GraphQL で手動で行った分析作業を自動化一発
⚠️ FFmpeg の異常値がある場所を探し
📊 そこを含む時間帯の、他のスタッツも採取し、状況を俯瞰

品質異常を検索

同時間帯のその他のメトリックとともに俯瞰グラフ化


品質の悪かった時間を探し、その時間の他のスコアを自動で採取し、俯瞰することができました。
手動でやっていたことをオフロードできたのは大きいです。
また、全体を見た分析の観点も付け加えられており、付加価値があります。

次回

ローカルでのテストはだいたい終わりました。
次にいつでもどこでも配信ができるように、Worker 環境を Cloudflare にデプロイします。
インターネットに置くので、認証認可をつけてセキュアに利用できる環境にしていきます。

しました

Discussion