Slack MCP Agentの実戦改善録

概要

Slack MCP Agent を Azure Container Apps にデプロイした結果、ログ解析や MCP Server の増減に手間がかかるなど運用上の課題が判明しました。 logging/AsyncExitStack/Secret Volume を導入してデバッグ性と動的拡張性を強化し、コスト最適化と権限管理の筋道を整理しました。 残る課題は SQLite 永続化・コスト削減の仕組み化・乱数による振る舞い制御であり、Blob 連携やジョブ駆動のポーリングを軸に今後改善を進めたいです。

はじめに

以前公開した Slackで動くMCP Agentを作った を実際に運用してみて、見えてきた課題と改善策をまとめました。 yutashx.hatenablog.com 読者の皆さんが同様のエージェントを本番環境で走らせる際のヒントになれば幸いです。

運用で可視化された課題

クラウド上での運用

MCP Server を MCP Host のサブプロセスとして起動し、標準入出力で通信する都合から Azure Container Apps を選択しました。

デバッグ性とログ監視

ローカルでは即座に原因が分かったエラーが、クラウド上では追跡に時間を要しました。 レスポンスが返らない原因がコンテナ・エージェント・MCP Server のどこにあるのか判定しづらく、セッション単位で再現しにくい不具合も散見されました。

MCP Server 追加の手間

新しい MCP Server を追加するたびに config.jsonapp.py の両方を更新する二重管理が発生していました。 API Key などの秘匿情報は config.json に集約したい一方で、config.jsonapp.pyの対応が崩れるとサーバーが起動せずエージェントも停止するという問題がありました。

データ永続化の欠如

Slack ユーザーと外部ツールのユーザーを突合させるための永続ストレージが存在せず、再起動のたびに紐付けが失われるという課題がありました。

改善アプローチ

標準化されたロギングと自己診断

Python 標準の logging モジュールでログを一元出力し、エージェント自身がそのログを読み取れる Function Tool を実装しました。 別セッションから障害セッションのログを即時に調査できるため、エージェントを使った原因究明ができるようになりました。

        @function_tool
        async def agent_log_reader() -> str:
            """Read the log file."""
            print("call agent_log_reader")
            with open("./log/app.log", "r") as f:
                return f.read()
        self.agent_log_reader = agent_log_reader

該当コード

AsyncExitStack による動的 MCP Server 追加

config.json にサーバー設定を追記してコンテナを再起動するだけで、新しい MCP Server が動的に読み込まれる仕組みを AsyncExitStack で実現しました。 秘匿情報は Azure Secret Volume で管理し、コード側の修正は不要です。

    async with contextlib.AsyncExitStack() as stack:
        # mcpServersのkey分だけMCPServerStdioを動的に生成し、ExitStackに登録
        servers = []
        for _, params in mcp_servers.items():
            server = await stack.enter_async_context(
                MCPServerStdio(params=params, cache_tools_list=True, client_session_timeout_seconds=60)
            )
            servers.append(server)

該当コード

Azure OpenAI 採用理由

  • 機密データ を保持でき、リージョンを日本国内に限定。
  • Terraform で IaC 化し、将来的に全てのリソースを管理したい。

アーキテクチャとシーケンス図

上記の改善を踏まえたアーキテクチャは以下のようになります。

flowchart TD
 subgraph Slack["Slack"]
        S["Slack Workspace"]
  end
 subgraph System["System Layer"]
        SYS["Event Router & State Manager"]
        CFG["Secret Volume (config.json)"]
  end
 subgraph AgentLayer["Agent Layer"]
        AG["Inference Agent & Agent Loop"]
        T1("Function Tools")
        SM("MCP Servers")
  end
  subgraph State
    LOG[("app.log")]
    DB[("SQLite")]
end
 subgraph Azure["Azure Container App"]
    direction TB
        System
        AgentLayer
        State
  end

    S -- app_mention --> SYS
    SYS --> AG
    SYS -- write --> LOG
    SYS -- SQL(read/write) --> DB
    CFG --> SYS
    AG -- call --> T1
    AG -- stdio spawn --> SM
    T1 -- SQL(read/write) --> DB
    T1 -- read --> LOG

またユーザーが Slack でエージェントにメンションすると、以下のような流れで処理が進みます。

sequenceDiagram
    participant U as ユーザー
    participant S as Slack
    participant A as Agent (app.py)
    participant MCP as MCP Servers
    participant FT as Function Tools
    participant DB as SQLite

    U->>S: メンション @agent 「〜して」
    S->>A: app_mention イベント
    A->>A: AsyncExitStack で<br>config.json を読み込み<br>各 MCP Server 起動
    A->>MCP: 必要な MCP Server に stdio リクエスト
    MCP-->>A: ツールリスト / 実行結果
    alt DB が必要な場合
        A->>FT: db_query()
        FT->>DB: SQL 実行
        DB-->>FT: rows
        FT-->>A: 結果
    end
    A-->>S: 途中経過「検索中…」
    A->>A: Runner.run() で推論完了
    A-->>S: 最終回答をスレッド返信

未解決課題と次の一手

SQLiteの永続化

現在は高速で手軽な SQLite を採用しているものの、コンテナごと削除するとデータが失われるという致命的な問題を抱えています。 この弱点を補うために、定期バッチで azcopy を実行し、ローカルの app.db を Azure Blob Storage へアップロードする方式を検討しています。 Blob を Container Apps に直接マウントできない制約は残るものの、バックアップ間隔を 1 時間程度に抑えれば実運用への影響は限定的だと判断しています。

権限管理

現状ではこのエージェントをSlack経由で誰でも利用することができます。 そのため誰でもエージェントに接続されているツールを利用することができてしまいます。 多数のユーザーがいるSlackチャンネルで利用する場合、権限管理をしっかりと行う必要があります。 ユーザーによる利用できるMCP Serverの制限を行う方法は、今回追加した動的にMCP Serverを追加する機能とDBの機能を組み合わせることで解決できると考えています。 あらかじめSlackのユーザー情報と使用させたいMCP Serverの対応表をDBに持っておき、Slackでメンションしたユーザーの情報を元に、DBからMCP Serverの情報を取得し、MCP Serverを動的に追加することで、エージェント側ではなく、その上位のシステム側で権限管理を行うことができるようになります。 ただし、今回追加したログ閲覧とメモリー機能でバイパスすることは可能なので、利便性とセキュリティのトレードオフを考慮する必要があります。

コストとパフォーマンス

0.5 vCPU・1 GiB を Replica 1 で常時稼働させると月額は約 1 万円に達します。 コールドスタートを許容できる時間帯が事前に読める場合は Replica 0 に設定し、外部ジョブから 1 分間隔でヘルスチェックを投げて必要なタイミングだけウォームアップする案が最も現実的だと考えています。

ランダムネスの入れ方

プロンプトで「たまにOOして」と指定しても、そのプロンプトを利用するエージェントは100%か0%の確率でしかOOを実行してくれないことが多いです。 おそらくセッションを跨いだ情報の共有ができていないため、頻度を調整することができないのだと思います。 セッション開始時にサーバー側で乱数を生成し、その結果を DB に保存してセッション単位で振る舞いを切り替える方式を検証中です。

まとめ

実運用で浮き彫りになった課題を 設計と運用の両面から改善し、使い勝手を向上させました。 今回分かったことは、エージェント側に全てを任せるのではなく、システム側でできることはシステム側でやる方が良いということです。 エージェントはどうしても再現性が低くなってしまうので、システム側で確実に実行されるように設計することが重要だと思いました。 今後も運用を続けながら、さらなる改善を目指していきます。