1. 背景:为什么不用 Windmill MCP 链接到标题
Windmill 提供 MCP Server,可以将脚本/流程暴露给 AI Agent。但实际使用中发现一个根本性问题:MCP 的脚本调用能力无法同时满足「传参」和「异步执行」两个需求。
| MCP 工具 | 传参 | 异步 | 说明 |
|---|---|---|---|
runScriptByPath |
❌ | ✅ | 只能传脚本路径,无法动态传入自定义参数 |
runScriptPreviewAndWaitResult |
✅ | ❌ | 支持传参,但会同步阻塞等待结果 |
s-f_demo_greeting (MCP 自动生成) |
✅ | ❌ | 同步等待,且 OpenCode 未暴露该工具 |
简单说:异步执行时不能传参,能传参的执行方式是同步等待。
这个限制来自 Windmill MCP 的实现:对于 runScriptByPath,底层虽然支持异步模式,但 MCP 工具定义中没有暴露 args 参数,导致调用时被忽略。
而 Windmill 原生 REST API 完全支持异步传参调用,因此考虑通过 OpenCode Plugin 直接封装 REST API 调用,绕过 MCP 的限制。
2. OpenCode Plugin 基本步骤 链接到标题
OpenCode 支持通过插件扩展工具能力。插件放置在项目或全局的 .opencode/plugins/ 目录下,无需在 opencode.json 中注册,OpenCode 会自动发现并加载。
目录结构 链接到标题
project/
├── .opencode/
│ └── plugins/
│ └── windmill-greeting.js # 插件文件
├── opencode.json # 无需配置插件路径
└── ...
插件基本结构 链接到标题
import { tool } from "@opencode-ai/plugin"
const MyPlugin = async () => {
return {
tool: {
my_tool_name: tool({
description: "工具描述,AI 会根据此描述决定何时调用",
args: {
param1: tool.schema.string(),
param2: tool.schema.number(),
},
async execute(args, context) {
// 工具逻辑,返回值会作为 AI 的观察结果
return { output: "结果内容" }
},
}),
},
}
}
export const MyPlugin
关键点:
- 使用
@opencode-ai/plugin的toolAPI 定义工具 description描述工具用途,AI 会据此判断何时调用args定义参数 schema,支持 string/number/boolean/array/objectexecute是异步函数,接收 args 和 context- 无需配置到
opencode.json,文件在正确目录即自动加载
3. 实现同步版本:验证插件可用 链接到标题
先实现一个最简单的版本,使用 Windmill 的 run_wait_result 端点,同步执行脚本并等待结果返回。
REST API 端点 链接到标题
POST http://<windmill-host>:3900/api/w/<workspace>/jobs/run_wait_result/p/<script-path>
该端点会同步阻塞,直到脚本执行完毕并返回结果。
同步版本代码 链接到标题
import { tool } from "@opencode-ai/plugin"
const WINDMILL_BASE_URL = "http://<windmill-host>:3900" // 或从环境变量获取
const API_TOKEN = process.env.WINDMILL_API_TOKEN // 或从环境变量/credentials 获取
const WORKSPACE = "default"
const SCRIPT_PATH = "f/demo/greeting"
export const WindmillGreetingPlugin = async () => {
return {
tool: {
windmill_greeting: tool({
description: "Execute Windmill greeting script synchronously and return the greeting text",
args: {
name: tool.schema.string(),
},
async execute(args, context) {
const url = `${WINDMILL_BASE_URL}/api/w/${WORKSPACE}/jobs/run_wait_result/p/${SCRIPT_PATH}`
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: args.name }),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Windmill API error ${response.status}: ${errorText}`)
}
const result = await response.json()
return { output: result }
},
}),
},
}
}
验证方式 链接到标题
部署插件后,通过自然语言触发:
运行 greeting 脚本,输入 name 为 张三
OpenCode 会自动调用 windmill_greeting 工具,传入参数,等待脚本执行完毕,返回问候结果。
4. 实现异步版本:验证可行性 链接到标题
同步版本适合执行时间较短的脚本。对于执行时间较长的任务(如视频处理、批量数据处理),同步等待会阻塞很久,需要改用异步模式。
异步两阶段设计 链接到标题
异步模式分两步:
- 提交阶段:调用
run端点,立即返回 Job ID,不等待执行完成 - 获取结果阶段:根据 Job ID 调用
get_result端点,获取执行结果
REST API 端点 链接到标题
# 异步提交(立即返回 Job ID)
POST http://<windmill-host>:3900/api/w/<workspace>/jobs/run/p/<script-path>
# 获取执行结果(需 Job 已完成)
GET http://<windmill-host>:3900/api/w/<workspace>/jobs_u/completed/get_result/<job-id>
异步版本代码 链接到标题
在同步版本基础上,增加两个工具:
import { tool } from "@opencode-ai/plugin"
const WINDMILL_BASE_URL = "http://<windmill-host>:3900" // 或从环境变量获取
const API_TOKEN = process.env.WINDMILL_API_TOKEN // 或从环境变量/credentials 获取
const WORKSPACE = "default"
const SCRIPT_PATH = "f/demo/greeting"
export const WindmillGreetingPlugin = async () => {
return {
tool: {
// 同步版本
windmill_greeting: tool({ ... }),
// 异步提交:返回 Job ID
windmill_greeting_async_submit: tool({
description: "Submit a greeting job to Windmill asynchronously and return the job ID. Use windmill_greeting_get_result to retrieve the result later.",
args: {
name: tool.schema.string(),
},
async execute(args, context) {
const url = `${WINDMILL_BASE_URL}/api/w/${WORKSPACE}/jobs/run/p/${SCRIPT_PATH}`
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: args.name }),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Windmill API error ${response.status}: ${errorText}`)
}
// 响应是纯文本 Job ID
const jobId = await response.text()
return { output: `Job submitted successfully. Job ID: ${jobId}` }
},
}),
// 获取结果:根据 Job ID 获取执行结果
windmill_greeting_get_result: tool({
description: "Get the result of a completed Windmill greeting job by its job ID",
args: {
job_id: tool.schema.string(),
},
async execute(args, context) {
const url = `${WINDMILL_BASE_URL}/api/w/${WORKSPACE}/jobs_u/completed/get_result/${args.job_id}`
const response = await fetch(url, {
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Windmill API error ${response.status}: ${errorText}`)
}
const result = await response.json()
return { output: result }
},
}),
},
}
}
异步模式使用示例 链接到标题
# 步骤 1:提交异步任务
运行 windmill_greeting_async_submit,输入 name 为 李四
# AI 会返回 Job ID:
# Job submitted successfully. Job ID: 019e634f-2d21-2686-5598-a0719f753a64
# 步骤 2:获取结果(需等待任务完成后)
运行 windmill_greeting_get_result,输入 job_id 为 019e634f-2d21-2686-5598-a0719f753a64
适用场景 链接到标题
| 模式 | 适用场景 | 等待时间 |
|---|---|---|
同步 (run_wait_result) |
问候、简单计算、快速查询 | < 10s |
异步 (run + get_result) |
视频处理、批量导出、长时间计算 | > 10s |
5. 总结与最佳实践 链接到标题
为什么不用 MCP 链接到标题
MCP 的 runScriptByPath 不支持动态传参,无法满足「异步 + 传参」的组合需求。通过 OpenCode Plugin 直接封装 REST API 是更灵活的方案。
插件优势 链接到标题
- 无需配置:文件在
.opencode/plugins/目录即自动加载 - 完全控制:可以封装任意 REST API 调用,不受 MCP 协议限制
- 灵活参数:参数 schema 完全自定义,AI 调用时自然语言即可映射
注意事项 链接到标题
-
获取结果时机:
windmill_greeting_get_result需要 Job 执行完成后才能获取结果。如果 Job 还在运行,会报错。建议配合 Agent 的等待/重试能力使用。 -
API Token 管理:示例中 Token 硬编码,实际项目中建议通过环境变量或 OpenCode credentials 管理。
-
Worker 标签:某些脚本需要特定环境(如 ffmpeg),需要通过 Windmill 的
tag参数指定到对应 Worker。