Auto-sync: 2026-03-01 17:00:01
This commit is contained in:
3
.env
3
.env
@@ -15,4 +15,5 @@ GOOGLE_OAUTH_CLIENT_SECRET=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
|
|||||||
|
|
||||||
# To rotate logs, set the following variables:
|
# To rotate logs, set the following variables:
|
||||||
#LOG_MAX_SIZE=10m
|
#LOG_MAX_SIZE=10m
|
||||||
#LOG_MAX_FILE=3
|
#LOG_MAX_FILE=3
|
||||||
|
WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh
|
||||||
|
|||||||
@@ -1,174 +1,202 @@
|
|||||||
x-logging: &default-logging
|
x-logging: &default-logging
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "${LOG_MAX_SIZE:-20m}"
|
max-size: "${LOG_MAX_SIZE:-20m}"
|
||||||
max-file: "${LOG_MAX_FILE:-10}"
|
max-file: "${LOG_MAX_FILE:-10}"
|
||||||
compress: "true"
|
compress: "true"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-net:
|
traefik-net:
|
||||||
external: true # サーバー上の既存Traefikネットワーク
|
external: true # サーバー上の既存Traefikネットワーク
|
||||||
windmill-internal:
|
windmill-internal:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
shm_size: 1g
|
shm_size: 1g
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/postgresql/data
|
- db_data:/var/lib/postgresql/data
|
||||||
expose:
|
expose:
|
||||||
- 5432
|
- 5432
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||||
POSTGRES_DB: windmill
|
POSTGRES_DB: windmill
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
networks:
|
networks:
|
||||||
- windmill-internal
|
- windmill-internal
|
||||||
|
|
||||||
windmill_server:
|
windmill_server:
|
||||||
image: ${WM_IMAGE}
|
image: ${WM_IMAGE}
|
||||||
container_name: windmill_server
|
container_name: windmill_server
|
||||||
pull_policy: if_not_present
|
pull_policy: if_not_present
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
- 8000
|
- 8000
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- MODE=server
|
- MODE=server
|
||||||
- BASE_URL=https://windmill.keinafarm.net
|
- BASE_URL=https://windmill.keinafarm.net
|
||||||
- OAUTH_REDIRECT_BASE_URL=https://windmill.keinafarm.net
|
- OAUTH_REDIRECT_BASE_URL=https://windmill.keinafarm.net
|
||||||
- GOOGLE_OAUTH_ENABLED=true
|
- GOOGLE_OAUTH_ENABLED=true
|
||||||
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
|
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
|
||||||
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET}
|
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- worker_logs:/tmp/windmill/logs
|
- worker_logs:/tmp/windmill/logs
|
||||||
# Git同期のために、カレントディレクトリ(リポジトリルート)を/workspaceにマウント
|
# Git同期のために、カレントディレクトリ(リポジトリルート)を/workspaceにマウント
|
||||||
# これにより、コンテナ内から .git ディレクトリにアクセス可能となり、git pushが可能になる
|
# これにより、コンテナ内から .git ディレクトリにアクセス可能となり、git pushが可能になる
|
||||||
- .:/workspace
|
- .:/workspace
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# HTTPSルーター
|
# HTTPSルーター
|
||||||
- "traefik.http.routers.windmill.rule=Host(`windmill.keinafarm.net`)"
|
- "traefik.http.routers.windmill.rule=Host(`windmill.keinafarm.net`)"
|
||||||
- "traefik.http.routers.windmill.entrypoints=websecure"
|
- "traefik.http.routers.windmill.entrypoints=websecure"
|
||||||
- "traefik.http.routers.windmill.tls=true"
|
- "traefik.http.routers.windmill.tls=true"
|
||||||
- "traefik.http.routers.windmill.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.windmill.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.services.windmill.loadbalancer.server.port=8000"
|
- "traefik.http.services.windmill.loadbalancer.server.port=8000"
|
||||||
# HTTPからHTTPSへのリダイレクト
|
# HTTPからHTTPSへのリダイレクト
|
||||||
- "traefik.http.routers.windmill-http.rule=Host(`windmill.keinafarm.net`)"
|
- "traefik.http.routers.windmill-http.rule=Host(`windmill.keinafarm.net`)"
|
||||||
- "traefik.http.routers.windmill-http.entrypoints=web"
|
- "traefik.http.routers.windmill-http.entrypoints=web"
|
||||||
- "traefik.http.routers.windmill-http.middlewares=windmill-https-redirect"
|
- "traefik.http.routers.windmill-http.middlewares=windmill-https-redirect"
|
||||||
- "traefik.http.middlewares.windmill-https-redirect.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.windmill-https-redirect.redirectscheme.scheme=https"
|
||||||
networks:
|
networks:
|
||||||
- traefik-net
|
- traefik-net
|
||||||
- windmill-internal
|
- windmill-internal
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
||||||
windmill_worker:
|
windmill_worker:
|
||||||
image: ${WM_IMAGE}
|
image: ${WM_IMAGE}
|
||||||
pull_policy: if_not_present
|
pull_policy: if_not_present
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 3
|
replicas: 3
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: "1"
|
cpus: "1"
|
||||||
memory: 2048M
|
memory: 2048M
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- MODE=worker
|
- MODE=worker
|
||||||
- WORKER_GROUP=default
|
- WORKER_GROUP=default
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- worker_dependency_cache:/tmp/windmill/cache
|
- worker_dependency_cache:/tmp/windmill/cache
|
||||||
- worker_logs:/tmp/windmill/logs
|
- worker_logs:/tmp/windmill/logs
|
||||||
# WorkerからもGit同期が必要な場合に備えてマウント
|
# WorkerからもGit同期が必要な場合に備えてマウント
|
||||||
- .:/workspace
|
- .:/workspace
|
||||||
networks:
|
networks:
|
||||||
- windmill-internal
|
- windmill-internal
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
||||||
windmill_worker_native:
|
windmill_worker_native:
|
||||||
image: ${WM_IMAGE}
|
image: ${WM_IMAGE}
|
||||||
pull_policy: if_not_present
|
pull_policy: if_not_present
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: "1"
|
cpus: "1"
|
||||||
memory: 2048M
|
memory: 2048M
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- MODE=worker
|
- MODE=worker
|
||||||
- WORKER_GROUP=native
|
- WORKER_GROUP=native
|
||||||
- NUM_WORKERS=8
|
- NUM_WORKERS=8
|
||||||
- SLEEP_QUEUE=200
|
- SLEEP_QUEUE=200
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- worker_logs:/tmp/windmill/logs
|
- worker_logs:/tmp/windmill/logs
|
||||||
networks:
|
networks:
|
||||||
- windmill-internal
|
- windmill-internal
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
||||||
windmill_extra:
|
windmill_extra:
|
||||||
image: ghcr.io/windmill-labs/windmill-extra:${WM_VERSION}
|
image: ghcr.io/windmill-labs/windmill-extra:${WM_VERSION}
|
||||||
pull_policy: if_not_present
|
pull_policy: if_not_present
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
- 3001
|
- 3001
|
||||||
- 3002
|
- 3002
|
||||||
- 3003
|
- 3003
|
||||||
environment:
|
environment:
|
||||||
- ENABLE_LSP=true
|
- ENABLE_LSP=true
|
||||||
- ENABLE_MULTIPLAYER=false
|
- ENABLE_MULTIPLAYER=false
|
||||||
- ENABLE_DEBUGGER=true
|
- ENABLE_DEBUGGER=true
|
||||||
- DEBUGGER_PORT=3003
|
- DEBUGGER_PORT=3003
|
||||||
- ENABLE_NSJAIL=false
|
- ENABLE_NSJAIL=false
|
||||||
- REQUIRE_SIGNED_DEBUG_REQUESTS=false
|
- REQUIRE_SIGNED_DEBUG_REQUESTS=false
|
||||||
- WINDMILL_BASE_URL=http://windmill_server:8000
|
- WINDMILL_BASE_URL=http://windmill_server:8000
|
||||||
volumes:
|
volumes:
|
||||||
- lsp_cache:/pyls/.cache
|
- lsp_cache:/pyls/.cache
|
||||||
networks:
|
networks:
|
||||||
- windmill-internal
|
- windmill-internal
|
||||||
- traefik-net
|
logging: *default-logging
|
||||||
logging: *default-logging
|
labels:
|
||||||
labels:
|
# LSPなどのWebSocket用設定(Caddyfileの代替)
|
||||||
# LSPなどのWebSocket用設定(Caddyfileの代替)
|
- "traefik.enable=true"
|
||||||
- "traefik.enable=true"
|
# LSPへのルーティング (/ws/* -> 3001)
|
||||||
# LSPへのルーティング (/ws/* -> 3001)
|
- "traefik.http.routers.windmill-lsp.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws/`)"
|
||||||
- "traefik.http.routers.windmill-lsp.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws/`)"
|
- "traefik.http.routers.windmill-lsp.entrypoints=websecure"
|
||||||
- "traefik.http.routers.windmill-lsp.entrypoints=websecure"
|
- "traefik.http.routers.windmill-lsp.tls=true"
|
||||||
- "traefik.http.routers.windmill-lsp.tls=true"
|
- "traefik.http.services.windmill-lsp.loadbalancer.server.port=3001"
|
||||||
- "traefik.http.routers.windmill-lsp.service=windmill-lsp"
|
# Debuggerへのルーティング (/ws_debug/* -> 3003)
|
||||||
- "traefik.http.services.windmill-lsp.loadbalancer.server.port=3001"
|
- "traefik.http.routers.windmill-debug.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws_debug/`)"
|
||||||
# Debuggerへのルーティング (/ws_debug/* -> 3003)
|
- "traefik.http.routers.windmill-debug.entrypoints=websecure"
|
||||||
- "traefik.http.routers.windmill-debug.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws_debug/`)"
|
- "traefik.http.routers.windmill-debug.tls=true"
|
||||||
- "traefik.http.routers.windmill-debug.entrypoints=websecure"
|
- "traefik.http.services.windmill-debug.loadbalancer.server.port=3003"
|
||||||
- "traefik.http.routers.windmill-debug.tls=true"
|
|
||||||
- "traefik.http.routers.windmill-debug.service=windmill-debug"
|
windmill_mcp:
|
||||||
- "traefik.http.services.windmill-debug.loadbalancer.server.port=3003"
|
build:
|
||||||
|
context: ./mcp
|
||||||
volumes:
|
dockerfile: Dockerfile
|
||||||
db_data: null
|
container_name: windmill_mcp
|
||||||
worker_dependency_cache: null
|
restart: unless-stopped
|
||||||
worker_logs: null
|
expose:
|
||||||
lsp_cache: null
|
- 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
|
||||||
|
worker_logs: null
|
||||||
|
lsp_cache: null
|
||||||
|
|||||||
14
mcp/Dockerfile
Normal file
14
mcp/Dockerfile
Normal file
@@ -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"]
|
||||||
2
mcp/requirements.txt
Normal file
2
mcp/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mcp>=1.0.0
|
||||||
|
httpx>=0.27.0
|
||||||
346
mcp/windmill_mcp.py
Normal file
346
mcp/windmill_mcp.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Windmill MCP Server - Claude が Windmill を直接操作できるようにする"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import httpx
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
WINDMILL_URL = os.environ.get("WINDMILL_URL", "https://windmill.keinafarm.net")
|
||||||
|
WINDMILL_TOKEN = os.environ.get("WINDMILL_TOKEN", "")
|
||||||
|
WINDMILL_WORKSPACE = os.environ.get("WINDMILL_WORKSPACE", "admins")
|
||||||
|
|
||||||
|
if not WINDMILL_TOKEN:
|
||||||
|
print("Error: WINDMILL_TOKEN 環境変数が設定されていません", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mcp = FastMCP("windmill")
|
||||||
|
|
||||||
|
|
||||||
|
def _headers() -> dict:
|
||||||
|
return {"Authorization": f"Bearer {WINDMILL_TOKEN}"}
|
||||||
|
|
||||||
|
|
||||||
|
def _api(path: str) -> str:
|
||||||
|
return f"{WINDMILL_URL}/api/w/{WINDMILL_WORKSPACE}/{path}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_list_flows(per_page: int = 20) -> str:
|
||||||
|
"""Windmill のフロー一覧を取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
per_page: 取得件数(最大100)
|
||||||
|
"""
|
||||||
|
resp = httpx.get(
|
||||||
|
_api("flows/list"),
|
||||||
|
headers=_headers(),
|
||||||
|
params={"per_page": min(per_page, 100)},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
flows = resp.json()
|
||||||
|
if not flows:
|
||||||
|
return "フローが見つかりませんでした"
|
||||||
|
lines = [
|
||||||
|
f"- {f['path']}: {f.get('summary', '(概要なし)')}" for f in flows
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_get_flow(path: str) -> str:
|
||||||
|
"""指定したパスのフロー定義(スクリプト含む)を取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: フローのパス (例: u/antigravity/git_sync)
|
||||||
|
"""
|
||||||
|
resp = httpx.get(_api(f"flows/get/{path}"), headers=_headers(), timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return json.dumps(resp.json(), indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_run_flow(path: str, args: str = "{}") -> str:
|
||||||
|
"""フローをトリガーして実行する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: フローのパス (例: u/antigravity/git_sync)
|
||||||
|
args: JSON形式の入力引数 (例: {"key": "value"})
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
args_dict = json.loads(args)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return f"Error: argsのJSON形式が不正です: {e}"
|
||||||
|
|
||||||
|
resp = httpx.post(
|
||||||
|
_api(f"jobs/run/f/{path}"),
|
||||||
|
headers=_headers(),
|
||||||
|
json=args_dict,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
job_id = resp.text.strip().strip('"')
|
||||||
|
return (
|
||||||
|
f"フローを開始しました。\n"
|
||||||
|
f"ジョブID: {job_id}\n"
|
||||||
|
f"詳細URL: {WINDMILL_URL}/run/{job_id}?workspace={WINDMILL_WORKSPACE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_list_recent_jobs(
|
||||||
|
limit: int = 20,
|
||||||
|
success_only: bool = False,
|
||||||
|
failure_only: bool = False,
|
||||||
|
script_path_filter: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""最近のジョブ一覧を取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: 取得件数(最大100)
|
||||||
|
success_only: Trueにすると成功ジョブのみ表示
|
||||||
|
failure_only: Trueにすると失敗ジョブのみ表示
|
||||||
|
script_path_filter: パスで絞り込む (例: u/antigravity/git_sync)
|
||||||
|
"""
|
||||||
|
params: dict = {"per_page": min(limit, 100)}
|
||||||
|
if success_only:
|
||||||
|
params["success"] = "true"
|
||||||
|
if failure_only:
|
||||||
|
params["success"] = "false"
|
||||||
|
if script_path_filter:
|
||||||
|
params["script_path_filter"] = script_path_filter
|
||||||
|
|
||||||
|
resp = httpx.get(_api("jobs/list"), headers=_headers(), params=params, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
jobs = resp.json()
|
||||||
|
if not jobs:
|
||||||
|
return "ジョブが見つかりませんでした"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for j in jobs:
|
||||||
|
success = j.get("success")
|
||||||
|
if success is True:
|
||||||
|
status = "[OK]"
|
||||||
|
elif success is False:
|
||||||
|
status = "[FAIL]"
|
||||||
|
else:
|
||||||
|
status = "[RUNNING]"
|
||||||
|
path = j.get("script_path", "unknown")
|
||||||
|
started = (j.get("started_at") or "")[:19] or "pending"
|
||||||
|
job_id = j.get("id", "")
|
||||||
|
lines.append(f"{status} [{started}] {path} (ID: {job_id})")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_get_job_logs(job_id: str) -> str:
|
||||||
|
"""ジョブの詳細情報とログを取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: ジョブのID(windmill_list_recent_jobs で確認できる)
|
||||||
|
"""
|
||||||
|
resp = httpx.get(_api(f"jobs_u/get/{job_id}"), headers=_headers(), timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
job = resp.json()
|
||||||
|
|
||||||
|
success = job.get("success")
|
||||||
|
if success is True:
|
||||||
|
state = "成功 [OK]"
|
||||||
|
elif success is False:
|
||||||
|
state = "失敗 [FAIL]"
|
||||||
|
else:
|
||||||
|
state = "実行中 [RUNNING]"
|
||||||
|
|
||||||
|
result_parts = [
|
||||||
|
f"ジョブID: {job_id}",
|
||||||
|
f"パス: {job.get('script_path', 'N/A')}",
|
||||||
|
f"状態: {state}",
|
||||||
|
f"開始: {job.get('started_at', 'N/A')}",
|
||||||
|
f"終了: {job.get('created_at', 'N/A')}",
|
||||||
|
]
|
||||||
|
|
||||||
|
log_resp = httpx.get(
|
||||||
|
_api(f"jobs_u/getlogs/{job_id}"), headers=_headers(), timeout=30
|
||||||
|
)
|
||||||
|
if log_resp.status_code == 200:
|
||||||
|
result_parts.append("\n--- ログ ---")
|
||||||
|
result_parts.append(log_resp.text)
|
||||||
|
|
||||||
|
result_val = job.get("result")
|
||||||
|
if result_val is not None:
|
||||||
|
result_parts.append("\n--- 実行結果 ---")
|
||||||
|
result_parts.append(
|
||||||
|
json.dumps(result_val, indent=2, ensure_ascii=False)
|
||||||
|
if isinstance(result_val, (dict, list))
|
||||||
|
else str(result_val)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 のスクリプト一覧を取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
per_page: 取得件数(最大100)
|
||||||
|
"""
|
||||||
|
resp = httpx.get(
|
||||||
|
_api("scripts/list"),
|
||||||
|
headers=_headers(),
|
||||||
|
params={"per_page": min(per_page, 100)},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
scripts = resp.json()
|
||||||
|
if not scripts:
|
||||||
|
return "スクリプトが見つかりませんでした"
|
||||||
|
lines = [
|
||||||
|
f"- {s['path']} [{s.get('language', '?')}]: {s.get('summary', '(概要なし)')}"
|
||||||
|
for s in scripts
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def windmill_get_script(path: str) -> str:
|
||||||
|
"""指定したパスのスクリプトのソースコードを取得する
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: スクリプトのパス (例: u/antigravity/test_git_sync)
|
||||||
|
"""
|
||||||
|
resp = httpx.get(_api(f"scripts/get/{path}"), headers=_headers(), timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
script = resp.json()
|
||||||
|
|
||||||
|
result_parts = [
|
||||||
|
f"パス: {script.get('path', 'N/A')}",
|
||||||
|
f"言語: {script.get('language', 'N/A')}",
|
||||||
|
f"概要: {script.get('summary', 'N/A')}",
|
||||||
|
"",
|
||||||
|
"--- コード ---",
|
||||||
|
script.get("content", "(コードなし)"),
|
||||||
|
]
|
||||||
|
return "\n".join(result_parts)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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")
|
||||||
Reference in New Issue
Block a user