From 2dbe8c8a742ccbf5786f8d1a91c0357476dba942 Mon Sep 17 00:00:00 2001 From: Akira Date: Mon, 2 Mar 2026 01:39:31 +0900 Subject: [PATCH] =?UTF-8?q?Windmill=20MCP=20=E3=82=B5=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=92=20SSE=20=E5=8C=96=E3=81=97=E3=81=A6=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E3=83=87=E3=83=97=E3=83=AD=E3=82=A4?= =?UTF-8?q?=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - windmill_mcp.py: MCP_TRANSPORT 環境変数で stdio/sse を切り替え可能に - mcp/Dockerfile: Python 3.12-slim ベースのコンテナイメージを追加 - docker-compose.yml: windmill_mcp サービスを追加(Traefik 経由で windmill-mcp.keinafarm.net に公開) Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 31 ++++++++++++ mcp/Dockerfile | 14 ++++++ mcp/windmill_mcp.py | 117 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 mcp/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 4fb00a8..db21349 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,6 +164,37 @@ services: - "traefik.http.routers.windmill-debug.tls=true" - "traefik.http.services.windmill-debug.loadbalancer.server.port=3003" + windmill_mcp: + build: + context: ./mcp + dockerfile: Dockerfile + container_name: windmill_mcp + restart: unless-stopped + expose: + - 8001 + environment: + - WINDMILL_TOKEN=${WINDMILL_TOKEN} + - WINDMILL_URL=https://windmill.keinafarm.net + - WINDMILL_WORKSPACE=admins + - MCP_TRANSPORT=sse + - MCP_HOST=0.0.0.0 + - MCP_PORT=8001 + labels: + - "traefik.enable=true" + # HTTPS ルーター + - "traefik.http.routers.windmill-mcp.rule=Host(`windmill-mcp.keinafarm.net`)" + - "traefik.http.routers.windmill-mcp.entrypoints=websecure" + - "traefik.http.routers.windmill-mcp.tls=true" + - "traefik.http.routers.windmill-mcp.tls.certresolver=letsencrypt" + - "traefik.http.services.windmill-mcp.loadbalancer.server.port=8001" + # HTTP → HTTPS リダイレクト + - "traefik.http.routers.windmill-mcp-http.rule=Host(`windmill-mcp.keinafarm.net`)" + - "traefik.http.routers.windmill-mcp-http.entrypoints=web" + - "traefik.http.routers.windmill-mcp-http.middlewares=windmill-https-redirect" + networks: + - traefik-net + logging: *default-logging + volumes: db_data: null worker_dependency_cache: null diff --git a/mcp/Dockerfile b/mcp/Dockerfile new file mode 100644 index 0000000..97ad8b0 --- /dev/null +++ b/mcp/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY windmill_mcp.py . + +ENV MCP_TRANSPORT=sse +ENV MCP_HOST=0.0.0.0 +ENV MCP_PORT=8001 + +CMD ["python", "windmill_mcp.py"] diff --git a/mcp/windmill_mcp.py b/mcp/windmill_mcp.py index 177c1ec..b9f4eed 100644 --- a/mcp/windmill_mcp.py +++ b/mcp/windmill_mcp.py @@ -181,6 +181,115 @@ def windmill_get_job_logs(job_id: str) -> str: return "\n".join(result_parts) +@mcp.tool() +def windmill_create_flow(path: str, summary: str, flow_definition: str, description: str = "") -> str: + """新しいフローを作成する + + Args: + path: フローのパス (例: u/admin/my_flow) + summary: フローの概要 + flow_definition: フローの定義 (JSON形式の文字列) + description: フローの詳細説明 (省略可) + """ + try: + flow_value = json.loads(flow_definition) + except json.JSONDecodeError as e: + return f"Error: flow_definitionのJSON形式が不正です: {e}" + + payload = { + "path": path, + "summary": summary, + "description": description, + "value": flow_value, + } + + resp = httpx.post( + _api("flows/create"), + headers=_headers(), + json=payload, + timeout=30, + ) + resp.raise_for_status() + return ( + f"フローを作成しました。\n" + f"パス: {path}\n" + f"URL: {WINDMILL_URL}/flows/edit/{path}?workspace={WINDMILL_WORKSPACE}" + ) + + +@mcp.tool() +def windmill_update_flow(path: str, summary: str, flow_definition: str, description: str = "") -> str: + """既存のフローを更新する + + Args: + path: フローのパス (例: u/admin/my_flow) + summary: フローの概要 + flow_definition: フローの定義 (JSON形式の文字列) + description: フローの詳細説明 (省略可) + """ + try: + flow_value = json.loads(flow_definition) + except json.JSONDecodeError as e: + return f"Error: flow_definitionのJSON形式が不正です: {e}" + + payload = { + "path": path, + "summary": summary, + "description": description, + "value": flow_value, + } + + resp = httpx.post( + _api(f"flows/edit/{path}"), + headers=_headers(), + json=payload, + timeout=30, + ) + resp.raise_for_status() + return ( + f"フローを更新しました。\n" + f"パス: {path}\n" + f"URL: {WINDMILL_URL}/flows/edit/{path}?workspace={WINDMILL_WORKSPACE}" + ) + + +@mcp.tool() +def windmill_create_script( + path: str, language: str, content: str, summary: str = "", description: str = "" +) -> str: + """新しいスクリプトを作成する(既存パスの場合は新バージョンを登録する) + + Args: + path: スクリプトのパス (例: u/admin/my_script) + language: 言語 (python3, deno, bun, bash など) + content: スクリプトのソースコード + summary: スクリプトの概要 (省略可) + description: スクリプトの詳細説明 (省略可) + """ + payload = { + "path": path, + "language": language, + "content": content, + "summary": summary, + "description": description, + } + + resp = httpx.post( + _api("scripts/create"), + headers=_headers(), + json=payload, + timeout=30, + ) + resp.raise_for_status() + hash_val = resp.text.strip().strip('"') + return ( + f"スクリプトを作成しました。\n" + f"パス: {path}\n" + f"ハッシュ: {hash_val}\n" + f"URL: {WINDMILL_URL}/scripts/edit/{path}?workspace={WINDMILL_WORKSPACE}" + ) + + @mcp.tool() def windmill_list_scripts(per_page: int = 20) -> str: """Windmill のスクリプト一覧を取得する @@ -228,4 +337,10 @@ def windmill_get_script(path: str) -> str: if __name__ == "__main__": - mcp.run(transport="stdio") + transport = os.environ.get("MCP_TRANSPORT", "stdio") + if transport == "sse": + host = os.environ.get("MCP_HOST", "0.0.0.0") + port = int(os.environ.get("MCP_PORT", "8001")) + mcp.run(transport="sse", host=host, port=port) + else: + mcp.run(transport="stdio")