Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7102f0f553 | ||
|
|
da2466462e | ||
|
|
77f3326868 | ||
|
|
2dbe8c8a74 | ||
|
|
c163d00f51 | ||
|
|
5e92ea62ef | ||
|
|
f80c2a2518 | ||
|
|
6de184968d | ||
|
|
be710b920e |
32
.claude/settings.local.json
Normal file
32
.claude/settings.local.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(curl -m 10 -s -o /dev/null -w \"%{http_code}\" -H \"Connection: Upgrade\" -H \"Upgrade: websocket\" https://windmill.keinafarm.net/ws/)",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" -t claude@keinafarm.net \"cat /home/windmill/windmill/docker-compose.yml\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/docker-compose.yml\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker compose --project-directory /home/windmill/windmill version 2>&1\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker compose --project-directory /home/windmill/windmill ps 2>&1\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/.env\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker network inspect traefik-net --format ''{{json .Name}}'' && docker network connect traefik-net windmill-windmill_extra-1 && echo ''Connected successfully''\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/docker-compose.yml > /tmp/windmill-compose.yml && cat /tmp/windmill-compose.yml | wc -l\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"grep -A 20 ''windmill_extra:'' /tmp/windmill-compose-fixed.yml | grep -E ''\\(traefik-net|windmill-lsp.service|windmill-debug.service\\)''\")",
|
||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"grep -n ''windmill-lsp\\\\|windmill-debug\\\\|traefik-net'' /tmp/windmill-compose-fixed.yml\")",
|
||||
"Bash(ssh -i:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"Bash(python -c \"from mcp.server.fastmcp import FastMCP; print\\(''mcp OK''\\)\")",
|
||||
"Bash(pip install mcp httpx)",
|
||||
"Bash(WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh python -c \":*)",
|
||||
"Read(//c/Users/akira/.claude/**)",
|
||||
"Bash(git add .gitignore mcp/)",
|
||||
"Bash(git commit:*)",
|
||||
"mcp__windmill__windmill_list_flows"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"C:\\Users\\akira\\Develop\\windmill",
|
||||
"C:\\Users\\akira\\.claude\\projects\\c--Users-akira-Develop-windmill\\memory\\"
|
||||
]
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"windmill"
|
||||
]
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -29,6 +29,9 @@ Thumbs.db
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# MCP server config (contains API tokens)
|
||||
.mcp.json
|
||||
|
||||
# Resolved markdown files (generated by editor)
|
||||
*.resolved
|
||||
*.resolved.*
|
||||
@@ -46,3 +49,11 @@ workflows/.wmill/tmp/
|
||||
!workflows/g/
|
||||
!workflows/wmill.yaml
|
||||
!workflows/wmill-lock.yaml
|
||||
__pycache__/
|
||||
|
||||
# Windmill workflow definitions (managed by git_sync on sync branch)
|
||||
# Local working directory should not track these on main
|
||||
u/
|
||||
workflows/f/
|
||||
workflows/u/
|
||||
workflows/g/
|
||||
|
||||
12
Caddyfile.local
Normal file
12
Caddyfile.local
Normal file
@@ -0,0 +1,12 @@
|
||||
:80 {
|
||||
# LSP - Language Server Protocol for code intelligence
|
||||
reverse_proxy /ws/* http://windmill_extra:3001
|
||||
|
||||
# Debugger - Interactive debugging via DAP WebSocket
|
||||
handle_path /ws_debug/* {
|
||||
reverse_proxy http://windmill_extra:3003
|
||||
}
|
||||
|
||||
# Default: Windmill server
|
||||
reverse_proxy /* http://windmill_server:8000
|
||||
}
|
||||
112
HANDOFF.md
Normal file
112
HANDOFF.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Windmill 作業引き継ぎメモ
|
||||
|
||||
> 作成: 2026-02-25(keinasystem_t02 セッションから移管)
|
||||
|
||||
---
|
||||
|
||||
## 現在の状態
|
||||
|
||||
### git_sync フロー(修正済み ✅)
|
||||
|
||||
**問題**: `u/antigravity/git_sync` フローが `wmill sync pull` で認証エラー
|
||||
**原因**: `$WM_TOKEN`(ジョブトークン)はワークスペーススコープのため、
|
||||
wmill CLIが内部で呼ぶ `/api/users/whoami`(グローバルAPI)で401
|
||||
**解決**:
|
||||
- `/home/windmill/windmill/wmill_config/remotes.ndjson` を作成(永続設定)
|
||||
- グローバルスコープトークン `CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8`(ラベル: `git-sync`)を使用
|
||||
- フロースクリプトを `--config-dir /workspace/wmill_config` を使う形に修正
|
||||
- 動作確認: Success: True (2秒) ✅
|
||||
|
||||
### windmill.keinafarm.net 外部アクセス(504 未修正 ❌)
|
||||
|
||||
- 外部からアクセスすると 30秒後に 504 Gateway Timeout
|
||||
- サーバー内部からは `http://localhost:8000` で正常にアクセスできる
|
||||
- **Caddyfile がこのディレクトリにある** → 原因調査・修正が必要
|
||||
|
||||
### Windmill MCP サーバー(未着手 ⬜)
|
||||
|
||||
- LLM(Claude)が直接 Windmill を操作できるようにする
|
||||
- 外部アクセスが直れば `https://windmill.keinafarm.net` に接続
|
||||
- 直らない場合は SSH トンネル経由
|
||||
|
||||
---
|
||||
|
||||
## 重要な情報
|
||||
|
||||
### サーバー接続
|
||||
|
||||
```
|
||||
SSH: root@keinafarm.net
|
||||
Windmill内部URL: http://localhost:8000
|
||||
Windmillサーバー上のパス: /home/windmill/windmill/
|
||||
```
|
||||
|
||||
### API トークン
|
||||
|
||||
| トークン | スコープ | 用途 |
|
||||
|---------|---------|------|
|
||||
| `qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh` | ワークスペース(admins) | 通常のAPI操作 |
|
||||
| `CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8` | グローバル | git-sync用(wmill CLI) |
|
||||
|
||||
### 2つの Git リポジトリ(別物!)
|
||||
|
||||
| リポジトリ | パス | 用途 |
|
||||
|-----------|------|------|
|
||||
| `windmill.git` (Gitea) | サーバー `/home/windmill/windmill/` | wmill CLI で自動同期 |
|
||||
| `windmill_workflow.git` (Gitea) | ローカル `C:\Users\akira\Develop\windmill` | このディレクトリ |
|
||||
|
||||
### wmill_config(永続設定)
|
||||
|
||||
```
|
||||
サーバーパス: /home/windmill/windmill/wmill_config/
|
||||
コンテナ内パス: /workspace/wmill_config/
|
||||
remotes.ndjson: {"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}
|
||||
activeWorkspace: admins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 次にやること(優先順)
|
||||
|
||||
### 1. windmill.keinafarm.net 504 を修正
|
||||
|
||||
`Caddyfile` を確認して、windmill へのリバースプロキシ設定を見直す。
|
||||
タイムアウト設定が足りない可能性が高い。
|
||||
|
||||
```
|
||||
# 確認コマンド(サーバー内部では正常)
|
||||
ssh root@keinafarm.net "curl -s http://localhost:8000/api/version"
|
||||
```
|
||||
|
||||
### 2. Windmill MCP サーバーを実装
|
||||
|
||||
**方針**: カスタム軽量 Python MCP サーバー(6〜8 tools)
|
||||
|
||||
実装する tools:
|
||||
- `windmill_list_flows` — フロー一覧
|
||||
- `windmill_get_flow` — フローのスクリプト取得
|
||||
- `windmill_run_flow` — フローをトリガー
|
||||
- `windmill_list_recent_jobs` — 最近のジョブ一覧(成功/失敗)
|
||||
- `windmill_get_job_logs` — ジョブの詳細ログ
|
||||
- `windmill_list_scripts` — スクリプト一覧
|
||||
- `windmill_get_script` — スクリプト取得
|
||||
|
||||
既存実装: `rothnic/windmill-mcp`(GitHub, スター0, 更新停止)→ 使わず自作
|
||||
|
||||
### 3. mail_filter.flow.json をコミット
|
||||
|
||||
ローカルの `windmill_workflow` に `f/mail/mail_filter.flow.json` が未コミット状態。
|
||||
|
||||
---
|
||||
|
||||
## 参考: wmill 設定ファイル形式
|
||||
|
||||
```
|
||||
~/.config/windmill/remotes.ndjson (1行1ワークスペース)
|
||||
{"remote":"http://...../","workspaceId":"...","name":"local_alias","token":"..."}
|
||||
|
||||
~/.config/windmill/activeWorkspace (プレーンテキスト)
|
||||
local_alias
|
||||
```
|
||||
|
||||
`wmill sync pull --config-dir /path/to/config` で任意ディレクトリを指定可能。
|
||||
@@ -8,10 +8,8 @@ x-logging: &default-logging
|
||||
compress: "true"
|
||||
|
||||
networks:
|
||||
traefik-net:
|
||||
external: true # Traefik管理下のネットワーク
|
||||
windmill-internal:
|
||||
driver: bridge # Windmill内部通信用
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
db:
|
||||
@@ -25,7 +23,7 @@ services:
|
||||
expose:
|
||||
- 5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: windmill
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||
@@ -39,46 +37,30 @@ services:
|
||||
windmill_server:
|
||||
image: ${WM_IMAGE}
|
||||
container_name: windmill_server
|
||||
pull_policy: always
|
||||
pull_policy: if_not_present
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8000
|
||||
- 2525
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- MODE=server
|
||||
- BASE_URL=http://localhost
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- worker_logs:/tmp/windmill/logs
|
||||
- /home/windmill/windmill:/workspace
|
||||
labels:
|
||||
# Traefik設定
|
||||
- "traefik.enable=true"
|
||||
# HTTPSルーター
|
||||
- "traefik.http.routers.windmill.rule=Host(`windmill.keinafarm.net`)"
|
||||
- "traefik.http.routers.windmill.entrypoints=websecure"
|
||||
- "traefik.http.routers.windmill.tls=true"
|
||||
- "traefik.http.routers.windmill.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.windmill.loadbalancer.server.port=8000"
|
||||
# HTTPからHTTPSへのリダイレクト
|
||||
- "traefik.http.routers.windmill-http.rule=Host(`windmill.keinafarm.net`)"
|
||||
- "traefik.http.routers.windmill-http.entrypoints=web"
|
||||
- "traefik.http.routers.windmill-http.middlewares=windmill-https-redirect"
|
||||
- "traefik.http.middlewares.windmill-https-redirect.redirectscheme.scheme=https"
|
||||
networks:
|
||||
- traefik-net
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
|
||||
windmill_worker:
|
||||
image: ${WM_IMAGE}
|
||||
pull_policy: always
|
||||
pull_policy: if_not_present
|
||||
deploy:
|
||||
replicas: 3
|
||||
replicas: 1
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1"
|
||||
@@ -95,14 +77,13 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- worker_dependency_cache:/tmp/windmill/cache
|
||||
- worker_logs:/tmp/windmill/logs
|
||||
- /home/windmill/windmill:/workspace
|
||||
networks:
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
|
||||
windmill_worker_native:
|
||||
image: ${WM_IMAGE}
|
||||
pull_policy: always
|
||||
pull_policy: if_not_present
|
||||
deploy:
|
||||
replicas: 1
|
||||
resources:
|
||||
@@ -125,35 +106,12 @@ services:
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
|
||||
windmill_indexer:
|
||||
image: ${WM_IMAGE}
|
||||
pull_policy: always
|
||||
deploy:
|
||||
replicas: 0 # 必要に応じて1に変更
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8002
|
||||
environment:
|
||||
- PORT=8002
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- MODE=indexer
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- windmill_index:/tmp/windmill/search
|
||||
- worker_logs:/tmp/windmill/logs
|
||||
networks:
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
|
||||
windmill_extra:
|
||||
image: ghcr.io/windmill-labs/windmill-extra:latest
|
||||
pull_policy: always
|
||||
image: ghcr.io/windmill-labs/windmill-extra:${WM_VERSION}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3001
|
||||
- 3002
|
||||
- 3003
|
||||
environment:
|
||||
- ENABLE_LSP=true
|
||||
@@ -168,15 +126,27 @@ services:
|
||||
networks:
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
# Caddyは使わない(Traefikを使用)
|
||||
# caddy:
|
||||
# deploy:
|
||||
# replicas: 0
|
||||
|
||||
caddy:
|
||||
image: caddy:2.9-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./Caddyfile.local:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- windmill-internal
|
||||
logging: *default-logging
|
||||
depends_on:
|
||||
- windmill_server
|
||||
- windmill_extra
|
||||
|
||||
volumes:
|
||||
db_data: null
|
||||
worker_dependency_cache: null
|
||||
worker_logs: null
|
||||
worker_memory: null
|
||||
windmill_index: null
|
||||
lsp_cache: null
|
||||
caddy_data: null
|
||||
caddy_config: null
|
||||
|
||||
@@ -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
|
||||
|
||||
12
mcp/.env.example
Normal file
12
mcp/.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Windmill MCP Server の設定
|
||||
# このファイルを .env にコピーして値を設定してください
|
||||
|
||||
# Windmill のベース URL(デフォルト: https://windmill.keinafarm.net)
|
||||
WINDMILL_URL=https://windmill.keinafarm.net
|
||||
|
||||
# Windmill API トークン(必須)
|
||||
# Windmill の「設定 > トークン」から作成してください
|
||||
WINDMILL_TOKEN=your_token_here
|
||||
|
||||
# 対象ワークスペース(デフォルト: admins)
|
||||
WINDMILL_WORKSPACE=admins
|
||||
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"]
|
||||
14
mcp/claude_mcp_config.json
Normal file
14
mcp/claude_mcp_config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"windmill": {
|
||||
"command": "python",
|
||||
"args": ["windmill_mcp.py"],
|
||||
"cwd": "/path/to/mcp",
|
||||
"env": {
|
||||
"WINDMILL_TOKEN": "your_api_token_here",
|
||||
"WINDMILL_URL": "https://windmill.keinafarm.net",
|
||||
"WINDMILL_WORKSPACE": "admins"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
mcp/requirements.txt
Normal file
2
mcp/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mcp>=1.0.0
|
||||
httpx>=0.27.0
|
||||
343
mcp/windmill_mcp.py
Normal file
343
mcp/windmill_mcp.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/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")
|
||||
MCP_HOST = os.environ.get("MCP_HOST", "127.0.0.1")
|
||||
MCP_PORT = int(os.environ.get("MCP_PORT", "8001"))
|
||||
|
||||
if not WINDMILL_TOKEN:
|
||||
print("Error: WINDMILL_TOKEN 環境変数が設定されていません", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mcp = FastMCP("windmill", host=MCP_HOST, port=MCP_PORT)
|
||||
|
||||
|
||||
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/update/{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")
|
||||
mcp.run(transport=transport)
|
||||
@@ -50,8 +50,10 @@ if [[ -n $(git status --porcelain) ]]; then
|
||||
git commit -m "Auto-sync: ${TIMESTAMP}"
|
||||
|
||||
echo -e "${YELLOW}Pushing to Gitea...${NC}"
|
||||
# リモートURLにトークンが含まれていない場合、プッシュに失敗する可能性がある
|
||||
# ここでは既存のoriginを使用
|
||||
# リモートの変更を先に取り込む(ローカルPCからのpushがある場合に備えて)
|
||||
git pull --rebase origin main || {
|
||||
echo -e "${RED}Failed to pull from remote. Trying push anyway...${NC}"
|
||||
}
|
||||
git push origin main || {
|
||||
echo -e "${RED}Failed to push. Need credentials in git remote url or credential helper.${NC}"
|
||||
echo -e "${YELLOW}Hint: git remote set-url origin https://<token>@gitea.keinafarm.net/...${NC}"
|
||||
|
||||
Reference in New Issue
Block a user