refactor(mcp): consolidate MCP implementation into internal package
Build and Deploy / build-and-push (push) Successful in 54s
Build and Deploy / build-and-push (push) Successful in 54s
- Move MCP tools registration to internal/mcp - cmd/mcp now only contains CLI entry logic - Support env vars TASKPOOL_URL/TASKPOOL_TOKEN for stdio mode - Remove duplicate client.go from cmd/mcp
This commit is contained in:
@@ -1,157 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client 调用 TaskPool OpenAPI (/open2api/v1)
|
|
||||||
type Client struct {
|
|
||||||
BaseURL string
|
|
||||||
Token string
|
|
||||||
HTTPClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(baseURL, token string) *Client {
|
|
||||||
return &Client{
|
|
||||||
BaseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"),
|
|
||||||
Token: strings.TrimSpace(token),
|
|
||||||
HTTPClient: &http.Client{
|
|
||||||
Timeout: 60 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) openBase() string {
|
|
||||||
return c.BaseURL + "/open2api/v1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) do(method, path string, query map[string]string, body any) (json.RawMessage, error) {
|
|
||||||
if c.BaseURL == "" {
|
|
||||||
return nil, fmt.Errorf("TASKPOOL_URL 未配置")
|
|
||||||
}
|
|
||||||
if c.Token == "" {
|
|
||||||
return nil, fmt.Errorf("TASKPOOL_TOKEN 未配置")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(c.openBase() + path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(query) > 0 {
|
|
||||||
q := u.Query()
|
|
||||||
for k, v := range query {
|
|
||||||
if strings.TrimSpace(v) != "" {
|
|
||||||
q.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
var reader io.Reader
|
|
||||||
if body != nil {
|
|
||||||
b, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
reader = bytes.NewReader(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, u.String(), reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
if body != nil {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("请求失败: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var envelope struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data json.RawMessage `json:"data"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(raw), 500))
|
|
||||||
}
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskPool 统一返回 HTTP 200,用 JSON body 中 code 区分成功/失败
|
|
||||||
// code=200 → 成功;其他 → 错误
|
|
||||||
if envelope.Code != 200 {
|
|
||||||
msg := envelope.Msg
|
|
||||||
if msg == "" {
|
|
||||||
msg = truncate(string(raw), 500)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("[%d] %s", envelope.Code, msg)
|
|
||||||
}
|
|
||||||
if len(envelope.Data) > 0 && string(envelope.Data) != "null" {
|
|
||||||
return envelope.Data, nil
|
|
||||||
}
|
|
||||||
// data 为空但请求成功(如 delete/stop 只返回 msg)
|
|
||||||
result := map[string]any{"code": envelope.Code, "msg": envelope.Msg}
|
|
||||||
b, _ := json.Marshal(result)
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Get(path string, query map[string]string) (json.RawMessage, error) {
|
|
||||||
return c.do(http.MethodGet, path, query, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Post(path string, body any) (json.RawMessage, error) {
|
|
||||||
return c.do(http.MethodPost, path, nil, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Put(path string, body any) (json.RawMessage, error) {
|
|
||||||
return c.do(http.MethodPut, path, nil, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Delete(path string) (json.RawMessage, error) {
|
|
||||||
return c.do(http.MethodDelete, path, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyJSON(v any) string {
|
|
||||||
b, err := json.MarshalIndent(v, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyRaw(raw json.RawMessage) string {
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return "null"
|
|
||||||
}
|
|
||||||
var v any
|
|
||||||
if err := json.Unmarshal(raw, &v); err != nil {
|
|
||||||
return string(raw)
|
|
||||||
}
|
|
||||||
return prettyJSON(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func truncate(s string, n int) string {
|
|
||||||
if len(s) <= n {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[:n] + "..."
|
|
||||||
}
|
|
||||||
+81
-605
@@ -1,21 +1,13 @@
|
|||||||
package mcp
|
package mcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/engigu/taskpool/internal/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
imc "github.com/engigu/taskpool/internal/mcp"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
serverName = "taskpool"
|
|
||||||
serverVersion = "1.0.0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run 启动 TaskPool MCP Server
|
// Run 启动 TaskPool MCP Server
|
||||||
@@ -28,14 +20,17 @@ const (
|
|||||||
// TASKPOOL_TOKEN 设置页 OpenAPI Token
|
// TASKPOOL_TOKEN 设置页 OpenAPI Token
|
||||||
// MCP_HTTP_ADDR HTTP 模式监听地址(默认 :8053)
|
// MCP_HTTP_ADDR HTTP 模式监听地址(默认 :8053)
|
||||||
func Run(args []string) {
|
func Run(args []string) {
|
||||||
mode := "stdio"
|
for _, a := range args {
|
||||||
httpAddr := ":8053"
|
|
||||||
|
|
||||||
for i, a := range args {
|
|
||||||
if a == "-h" || a == "--help" {
|
if a == "-h" || a == "--help" {
|
||||||
printHelp()
|
printHelp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := "stdio"
|
||||||
|
httpAddr := ":8053"
|
||||||
|
|
||||||
|
for i, a := range args {
|
||||||
if a == "--http" {
|
if a == "--http" {
|
||||||
mode = "http"
|
mode = "http"
|
||||||
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||||
@@ -44,627 +39,60 @@ func Run(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP 模式优先从环境变量读取地址
|
|
||||||
if envAddr := os.Getenv("MCP_HTTP_ADDR"); envAddr != "" {
|
if envAddr := os.Getenv("MCP_HTTP_ADDR"); envAddr != "" {
|
||||||
httpAddr = envAddr
|
httpAddr = envAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL := firstNonEmpty(os.Getenv("TASKPOOL_URL"), os.Getenv("BAIHU_URL"), "http://127.0.0.1:8052")
|
|
||||||
token := firstNonEmpty(os.Getenv("TASKPOOL_TOKEN"), os.Getenv("BAIHU_TOKEN"), os.Getenv("OPENAPI_TOKEN"))
|
|
||||||
|
|
||||||
client := NewClient(baseURL, token)
|
|
||||||
s := server.NewMCPServer(
|
|
||||||
serverName,
|
|
||||||
serverVersion,
|
|
||||||
server.WithToolCapabilities(true),
|
|
||||||
server.WithResourceCapabilities(false, false),
|
|
||||||
server.WithPromptCapabilities(true),
|
|
||||||
server.WithInstructions(instructions),
|
|
||||||
server.WithRecovery(),
|
|
||||||
)
|
|
||||||
|
|
||||||
registerTools(s, client)
|
|
||||||
registerPrompts(s)
|
|
||||||
|
|
||||||
if mode == "http" {
|
if mode == "http" {
|
||||||
runHTTP(s, httpAddr, client)
|
runHTTP(httpAddr)
|
||||||
} else {
|
} else {
|
||||||
runStdio(s, client)
|
runStdio()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStdio(s *server.MCPServer, client *Client) {
|
func runStdio() {
|
||||||
// 日志只能走 stderr,避免污染 stdio JSON-RPC
|
// 使用内部 OpenAPI 客户端(自动获取服务器地址和 Token)
|
||||||
|
client := imc.GetOpenAPIClient()
|
||||||
fmt.Fprintf(os.Stderr, "[taskpool-mcp] mode=stdio url=%s token=%s\n", client.BaseURL, maskToken(client.Token))
|
fmt.Fprintf(os.Stderr, "[taskpool-mcp] mode=stdio url=%s token=%s\n", client.BaseURL, maskToken(client.Token))
|
||||||
|
|
||||||
if err := server.ServeStdio(s); err != nil {
|
s := mcp.GetServer()
|
||||||
|
// 设置外部客户端(用于 stdio 模式连接远程服务器)
|
||||||
|
if client.Token != "" {
|
||||||
|
mcp.SetExternalClient(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mcp.ServeStdio(s); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[taskpool-mcp] server error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "[taskpool-mcp] server error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHTTP(s *server.MCPServer, addr string, client *Client) {
|
func runHTTP(addr string) {
|
||||||
|
client := imc.GetOpenAPIClient()
|
||||||
fmt.Fprintf(os.Stderr, "[taskpool-mcp] mode=http addr=%s url=%s token=%s\n", addr, client.BaseURL, maskToken(client.Token))
|
fmt.Fprintf(os.Stderr, "[taskpool-mcp] mode=http addr=%s url=%s token=%s\n", addr, client.BaseURL, maskToken(client.Token))
|
||||||
|
|
||||||
httpServer := server.NewStreamableHTTPServer(s,
|
// 设置外部客户端
|
||||||
server.WithEndpointPath("/mcp"),
|
if client.Token != "" {
|
||||||
)
|
mcp.SetExternalClient(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := mcp.GetHTTPHandler()
|
||||||
|
|
||||||
// 添加健康检查端点
|
// 添加健康检查端点
|
||||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/mcp/", http.StripPrefix("/mcp", handler))
|
||||||
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(`{"status":"ok"}`))
|
w.Write([]byte(`{"status":"ok"}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "[taskpool-mcp] MCP endpoint: http://%s/mcp\n", strings.TrimPrefix(addr, ":"))
|
fmt.Fprintf(os.Stderr, "[taskpool-mcp] MCP endpoint: http://%s/mcp\n", strings.TrimPrefix(addr, ":"))
|
||||||
if err := httpServer.Start(addr); err != nil {
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[taskpool-mcp] http server error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "[taskpool-mcp] http server error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printHelp() {
|
|
||||||
fmt.Fprintf(os.Stderr, `
|
|
||||||
TaskPool MCP Server
|
|
||||||
|
|
||||||
通过 MCP 协议把 TaskPool OpenAPI 暴露给 Hermes / OpenClaw / Cursor 等 Agent。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
taskpool mcp # stdio 模式(默认),适用于本地 Agent
|
|
||||||
taskpool mcp --http # HTTP 模式,监听 :8053
|
|
||||||
taskpool mcp --http :9000 # HTTP 模式,监听指定端口
|
|
||||||
|
|
||||||
环境变量:
|
|
||||||
TASKPOOL_URL 面板地址 (默认 http://127.0.0.1:8052)
|
|
||||||
TASKPOOL_TOKEN 设置页生成的 OpenAPI Token(必填)
|
|
||||||
MCP_HTTP_ADDR HTTP 模式监听地址(默认 :8053)
|
|
||||||
|
|
||||||
模式说明:
|
|
||||||
stdio 通过 stdin/stdout 通信,需要本地安装 taskpool 二进制
|
|
||||||
http 通过 HTTP/SSE 通信,支持远程访问,无需本地安装
|
|
||||||
|
|
||||||
--- stdio 模式配置示例 ---
|
|
||||||
Hermes / OpenClaw / Cursor 配置:
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"taskpool": {
|
|
||||||
"command": "taskpool",
|
|
||||||
"args": ["mcp"],
|
|
||||||
"env": {
|
|
||||||
"TASKPOOL_URL": "http://127.0.0.1:8052",
|
|
||||||
"TASKPOOL_TOKEN": "你的 OpenAPI Token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
--- HTTP 模式配置示例 ---
|
|
||||||
1. 启动 MCP HTTP Server:
|
|
||||||
taskpool mcp --http :8053
|
|
||||||
|
|
||||||
2. Hermes / OpenClaw 配置(使用 URL):
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"taskpool": {
|
|
||||||
"url": "http://your-server:8053/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
注意: HTTP 模式建议配置反向代理(如 Nginx)添加 HTTPS 和认证
|
|
||||||
|
|
||||||
前置条件:
|
|
||||||
1. 面板「系统设置」启用 OpenAPI 并生成 Token
|
|
||||||
2. 后端服务已启动 (taskpool server)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const instructions = `你是 TaskPool(任务池)运维助手。通过 MCP tools 管理任务调度面板。
|
|
||||||
|
|
||||||
核心能力:
|
|
||||||
- 任务:列表/详情/创建/更新/删除/启停执行
|
|
||||||
- 脚本:列表/详情/创建/更新/删除
|
|
||||||
- 环境变量:列表/详情/创建/更新/删除(secret 值为脱敏)
|
|
||||||
- 日志:列表/详情、最近执行结果
|
|
||||||
|
|
||||||
使用原则:
|
|
||||||
1. 先 list_tasks / get_task 确认目标,再 run_task
|
|
||||||
2. 删除类操作前向用户确认
|
|
||||||
3. 不要编造任务 ID;ID 以 list/get 返回为准
|
|
||||||
4. 执行后用 list_logs 或 get_log 查看结果
|
|
||||||
5. 创建任务时 command 必填(普通任务);schedule 使用标准 cron`
|
|
||||||
|
|
||||||
func registerTools(s *server.MCPServer, c *Client) {
|
|
||||||
// ---- tasks ----
|
|
||||||
s.AddTool(mcp.NewTool("list_tasks",
|
|
||||||
mcp.WithDescription("分页列出任务,可按名称/标签/启用状态筛选"),
|
|
||||||
mcp.WithString("name", mcp.Description("按名称模糊筛选")),
|
|
||||||
mcp.WithString("tags", mcp.Description("按标签筛选")),
|
|
||||||
mcp.WithString("enabled", mcp.Description("true/false,是否启用")),
|
|
||||||
mcp.WithNumber("page", mcp.Description("页码,默认 1")),
|
|
||||||
mcp.WithNumber("page_size", mcp.Description("每页数量,默认 20")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
q := map[string]string{
|
|
||||||
"name": strArg(req, "name"),
|
|
||||||
"tags": strArg(req, "tags"),
|
|
||||||
"enabled": strArg(req, "enabled"),
|
|
||||||
"page": numArgStr(req, "page", "1"),
|
|
||||||
"page_size": numArgStr(req, "page_size", "20"),
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/tasks", q)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_task",
|
|
||||||
mcp.WithDescription("获取单个任务详情"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("任务 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/tasks/"+id, nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("create_task",
|
|
||||||
mcp.WithDescription("创建任务。普通任务必须提供 command;可选 schedule(cron)、tags、timeout、work_dir、remark、enabled 等"),
|
|
||||||
mcp.WithString("name", mcp.Required(), mcp.Description("任务名称")),
|
|
||||||
mcp.WithString("command", mcp.Description("执行命令,普通任务必填")),
|
|
||||||
mcp.WithString("schedule", mcp.Description("Cron 表达式,如 0 0 * * *")),
|
|
||||||
mcp.WithString("remark", mcp.Description("备注")),
|
|
||||||
mcp.WithString("tags", mcp.Description("标签,逗号分隔")),
|
|
||||||
mcp.WithString("work_dir", mcp.Description("工作目录")),
|
|
||||||
mcp.WithNumber("timeout", mcp.Description("超时秒数")),
|
|
||||||
mcp.WithString("type", mcp.Description("任务类型,默认 common;仓库同步为 repo")),
|
|
||||||
mcp.WithString("config", mcp.Description("JSON 字符串配置(repo 任务用)")),
|
|
||||||
mcp.WithString("envs", mcp.Description("环境变量 JSON 字符串")),
|
|
||||||
mcp.WithString("trigger_type", mcp.Description("触发类型,如 cron")),
|
|
||||||
mcp.WithNumber("retry_count", mcp.Description("重试次数")),
|
|
||||||
mcp.WithNumber("retry_interval", mcp.Description("重试间隔秒")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
body := map[string]any{}
|
|
||||||
setStr(body, "name", strArg(req, "name"))
|
|
||||||
setStr(body, "command", strArg(req, "command"))
|
|
||||||
setStr(body, "schedule", strArg(req, "schedule"))
|
|
||||||
setStr(body, "remark", strArg(req, "remark"))
|
|
||||||
setStr(body, "tags", strArg(req, "tags"))
|
|
||||||
setStr(body, "work_dir", strArg(req, "work_dir"))
|
|
||||||
setStr(body, "type", strArg(req, "type"))
|
|
||||||
setStr(body, "config", strArg(req, "config"))
|
|
||||||
setStr(body, "envs", strArg(req, "envs"))
|
|
||||||
setStr(body, "trigger_type", strArg(req, "trigger_type"))
|
|
||||||
if n, ok := numArg(req, "timeout"); ok {
|
|
||||||
body["timeout"] = n
|
|
||||||
}
|
|
||||||
if n, ok := numArg(req, "retry_count"); ok {
|
|
||||||
body["retry_count"] = n
|
|
||||||
}
|
|
||||||
if n, ok := numArg(req, "retry_interval"); ok {
|
|
||||||
body["retry_interval"] = n
|
|
||||||
}
|
|
||||||
if body["name"] == nil || body["name"] == "" {
|
|
||||||
return mcp.NewToolResultError("name 必填"), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Post("/tasks", body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("update_task",
|
|
||||||
mcp.WithDescription("更新任务字段(只传需要改的字段)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("任务 ID")),
|
|
||||||
mcp.WithString("name", mcp.Description("任务名称")),
|
|
||||||
mcp.WithString("command", mcp.Description("命令")),
|
|
||||||
mcp.WithString("schedule", mcp.Description("Cron")),
|
|
||||||
mcp.WithString("remark", mcp.Description("备注")),
|
|
||||||
mcp.WithString("tags", mcp.Description("标签")),
|
|
||||||
mcp.WithString("work_dir", mcp.Description("工作目录")),
|
|
||||||
mcp.WithString("enabled", mcp.Description("true/false")),
|
|
||||||
mcp.WithNumber("timeout", mcp.Description("超时秒数")),
|
|
||||||
mcp.WithString("envs", mcp.Description("环境变量 JSON 字符串")),
|
|
||||||
mcp.WithString("type", mcp.Description("任务类型")),
|
|
||||||
mcp.WithString("config", mcp.Description("配置 JSON 字符串")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
body := map[string]any{}
|
|
||||||
setStr(body, "name", strArg(req, "name"))
|
|
||||||
setStr(body, "command", strArg(req, "command"))
|
|
||||||
setStr(body, "schedule", strArg(req, "schedule"))
|
|
||||||
setStr(body, "remark", strArg(req, "remark"))
|
|
||||||
setStr(body, "tags", strArg(req, "tags"))
|
|
||||||
setStr(body, "work_dir", strArg(req, "work_dir"))
|
|
||||||
setStr(body, "envs", strArg(req, "envs"))
|
|
||||||
setStr(body, "type", strArg(req, "type"))
|
|
||||||
setStr(body, "config", strArg(req, "config"))
|
|
||||||
if e := strArg(req, "enabled"); e != "" {
|
|
||||||
body["enabled"] = strings.EqualFold(e, "true") || e == "1"
|
|
||||||
}
|
|
||||||
if n, ok := numArg(req, "timeout"); ok {
|
|
||||||
body["timeout"] = n
|
|
||||||
}
|
|
||||||
raw, err := c.Put("/tasks/"+id, body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("delete_task",
|
|
||||||
mcp.WithDescription("删除任务(危险操作,执行前请确认)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("任务 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Delete("/tasks/" + id)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("list_task_tags",
|
|
||||||
mcp.WithDescription("获取所有任务标签"),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
raw, err := c.Get("/tasks/tags", nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("run_task",
|
|
||||||
mcp.WithDescription("立即执行指定任务。可选传入临时环境变量 envs_json"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("任务 ID")),
|
|
||||||
mcp.WithString("envs_json", mcp.Description("临时环境变量 JSON 对象字符串,如 {\"FOO\":\"bar\"}")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
var body any
|
|
||||||
if ej := strArg(req, "envs_json"); ej != "" {
|
|
||||||
var envs map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(ej), &envs); err != nil {
|
|
||||||
return mcp.NewToolResultError("envs_json 不是合法 JSON 对象: " + err.Error()), nil
|
|
||||||
}
|
|
||||||
body = map[string]any{"envs": envs}
|
|
||||||
} else {
|
|
||||||
body = map[string]any{}
|
|
||||||
}
|
|
||||||
raw, err := c.Post("/execute/task/"+id, body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("stop_task",
|
|
||||||
mcp.WithDescription("停止正在运行的任务(需要执行日志 log_id)"),
|
|
||||||
mcp.WithString("log_id", mcp.Required(), mcp.Description("运行日志 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
logID, err := req.RequireString("log_id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Post("/tasks/stop/"+logID, map[string]any{})
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_last_results",
|
|
||||||
mcp.WithDescription("获取最近若干次执行结果摘要"),
|
|
||||||
mcp.WithNumber("count", mcp.Description("数量,默认 10")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
q := map[string]string{"count": numArgStr(req, "count", "10")}
|
|
||||||
raw, err := c.Get("/execute/results", q)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- logs ----
|
|
||||||
s.AddTool(mcp.NewTool("list_logs",
|
|
||||||
mcp.WithDescription("分页查询任务执行日志"),
|
|
||||||
mcp.WithString("task_id", mcp.Description("任务 ID")),
|
|
||||||
mcp.WithString("task_name", mcp.Description("任务名称模糊")),
|
|
||||||
mcp.WithString("status", mcp.Description("状态筛选")),
|
|
||||||
mcp.WithNumber("page", mcp.Description("页码,默认 1")),
|
|
||||||
mcp.WithNumber("page_size", mcp.Description("每页数量,默认 20")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
q := map[string]string{
|
|
||||||
"task_id": strArg(req, "task_id"),
|
|
||||||
"task_name": strArg(req, "task_name"),
|
|
||||||
"status": strArg(req, "status"),
|
|
||||||
"page": numArgStr(req, "page", "1"),
|
|
||||||
"page_size": numArgStr(req, "page_size", "20"),
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/logs", q)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_log",
|
|
||||||
mcp.WithDescription("获取单条执行日志详情(含输出)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("日志 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/logs/"+id, nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- scripts ----
|
|
||||||
s.AddTool(mcp.NewTool("list_scripts",
|
|
||||||
mcp.WithDescription("列出脚本(不含内容)"),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
raw, err := c.Get("/scripts", nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_script",
|
|
||||||
mcp.WithDescription("获取脚本详情(含内容)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("脚本 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/scripts/"+id, nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("create_script",
|
|
||||||
mcp.WithDescription("创建脚本"),
|
|
||||||
mcp.WithString("name", mcp.Required(), mcp.Description("脚本名称/路径")),
|
|
||||||
mcp.WithString("content", mcp.Required(), mcp.Description("脚本内容")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
name, err := req.RequireString("name")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
content, err := req.RequireString("content")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Post("/scripts", map[string]any{"name": name, "content": content})
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("update_script",
|
|
||||||
mcp.WithDescription("更新脚本"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("脚本 ID")),
|
|
||||||
mcp.WithString("name", mcp.Description("名称")),
|
|
||||||
mcp.WithString("content", mcp.Description("内容")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
body := map[string]any{}
|
|
||||||
setStr(body, "name", strArg(req, "name"))
|
|
||||||
setStr(body, "content", strArg(req, "content"))
|
|
||||||
raw, err := c.Put("/scripts/"+id, body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("delete_script",
|
|
||||||
mcp.WithDescription("删除脚本(危险操作)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("脚本 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Delete("/scripts/" + id)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- env ----
|
|
||||||
s.AddTool(mcp.NewTool("list_envs",
|
|
||||||
mcp.WithDescription("分页列出环境变量(secret 值已脱敏)"),
|
|
||||||
mcp.WithString("name", mcp.Description("按名称筛选")),
|
|
||||||
mcp.WithString("type", mcp.Description("类型筛选")),
|
|
||||||
mcp.WithString("tags", mcp.Description("标签筛选")),
|
|
||||||
mcp.WithNumber("page", mcp.Description("页码")),
|
|
||||||
mcp.WithNumber("page_size", mcp.Description("每页数量")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
q := map[string]string{
|
|
||||||
"name": strArg(req, "name"),
|
|
||||||
"type": strArg(req, "type"),
|
|
||||||
"tags": strArg(req, "tags"),
|
|
||||||
"page": numArgStr(req, "page", "1"),
|
|
||||||
"page_size": numArgStr(req, "page_size", "20"),
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/env", q)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("list_all_envs",
|
|
||||||
mcp.WithDescription("获取全部环境变量(不分页,secret 脱敏)"),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
raw, err := c.Get("/env/all", nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_env",
|
|
||||||
mcp.WithDescription("获取环境变量详情"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("环境变量 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Get("/env/"+id, nil)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("create_env",
|
|
||||||
mcp.WithDescription("创建环境变量"),
|
|
||||||
mcp.WithString("name", mcp.Required(), mcp.Description("变量名")),
|
|
||||||
mcp.WithString("value", mcp.Required(), mcp.Description("变量值")),
|
|
||||||
mcp.WithString("type", mcp.Description("类型,如 text/secret")),
|
|
||||||
mcp.WithString("remark", mcp.Description("备注")),
|
|
||||||
mcp.WithString("tags", mcp.Description("标签")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
name, err := req.RequireString("name")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
value, err := req.RequireString("value")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
body := map[string]any{"name": name, "value": value}
|
|
||||||
setStr(body, "type", strArg(req, "type"))
|
|
||||||
setStr(body, "remark", strArg(req, "remark"))
|
|
||||||
setStr(body, "tags", strArg(req, "tags"))
|
|
||||||
raw, err := c.Post("/env", body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("update_env",
|
|
||||||
mcp.WithDescription("更新环境变量"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("环境变量 ID")),
|
|
||||||
mcp.WithString("name", mcp.Description("变量名")),
|
|
||||||
mcp.WithString("value", mcp.Description("变量值")),
|
|
||||||
mcp.WithString("type", mcp.Description("类型")),
|
|
||||||
mcp.WithString("remark", mcp.Description("备注")),
|
|
||||||
mcp.WithString("tags", mcp.Description("标签")),
|
|
||||||
mcp.WithString("enabled", mcp.Description("true/false")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
body := map[string]any{}
|
|
||||||
setStr(body, "name", strArg(req, "name"))
|
|
||||||
setStr(body, "value", strArg(req, "value"))
|
|
||||||
setStr(body, "type", strArg(req, "type"))
|
|
||||||
setStr(body, "remark", strArg(req, "remark"))
|
|
||||||
setStr(body, "tags", strArg(req, "tags"))
|
|
||||||
if e := strArg(req, "enabled"); e != "" {
|
|
||||||
body["enabled"] = strings.EqualFold(e, "true") || e == "1"
|
|
||||||
}
|
|
||||||
raw, err := c.Put("/env/"+id, body)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("delete_env",
|
|
||||||
mcp.WithDescription("删除环境变量(危险操作)"),
|
|
||||||
mcp.WithString("id", mcp.Required(), mcp.Description("环境变量 ID")),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
id, err := req.RequireString("id")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
raw, err := c.Delete("/env/" + id)
|
|
||||||
return toolResult(raw, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- meta ----
|
|
||||||
s.AddTool(mcp.NewTool("ping_api",
|
|
||||||
mcp.WithDescription("检查 OpenAPI 连通性与鉴权是否有效(通过 list_tasks page_size=1)"),
|
|
||||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
raw, err := c.Get("/tasks", map[string]string{"page": "1", "page_size": "1"})
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError("连通失败: " + err.Error()), nil
|
|
||||||
}
|
|
||||||
return mcp.NewToolResultText("OpenAPI 连通正常\n" + prettyRaw(raw)), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerPrompts(s *server.MCPServer) {
|
|
||||||
s.AddPrompt(mcp.NewPrompt("ops_overview",
|
|
||||||
mcp.WithPromptDescription("巡检任务池:列出任务与最近失败日志"),
|
|
||||||
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
|
||||||
return mcp.NewGetPromptResult(
|
|
||||||
"TaskPool 巡检",
|
|
||||||
[]mcp.PromptMessage{
|
|
||||||
mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(
|
|
||||||
"请使用 TaskPool MCP:1) list_tasks 查看任务概况 2) list_logs 筛选失败状态 3) 对重要失败 get_log 总结原因并给出是否建议 run_task 重跑。",
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
s.AddPrompt(mcp.NewPrompt("run_and_check",
|
|
||||||
mcp.WithPromptDescription("执行指定任务并检查日志"),
|
|
||||||
mcp.WithArgument("task_id", mcp.RequiredArgument(), mcp.ArgumentDescription("任务 ID")),
|
|
||||||
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
|
||||||
taskID := ""
|
|
||||||
if request.Params.Arguments != nil {
|
|
||||||
taskID = request.Params.Arguments["task_id"]
|
|
||||||
}
|
|
||||||
text := fmt.Sprintf("请对 TaskPool 任务 %s:1) get_task 确认 2) run_task 执行 3) list_logs(task_id) 取最新日志 4) get_log 汇总输出与状态。", taskID)
|
|
||||||
return mcp.NewGetPromptResult(
|
|
||||||
"执行并检查",
|
|
||||||
[]mcp.PromptMessage{
|
|
||||||
mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(text)),
|
|
||||||
},
|
|
||||||
), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func toolResult(raw json.RawMessage, err error) (*mcp.CallToolResult, error) {
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
|
||||||
}
|
|
||||||
return mcp.NewToolResultText(prettyRaw(raw)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func strArg(req mcp.CallToolRequest, key string) string {
|
|
||||||
v, err := req.RequireString(key)
|
|
||||||
if err != nil {
|
|
||||||
// optional
|
|
||||||
if args, ok := req.Params.Arguments.(map[string]any); ok {
|
|
||||||
if x, ok := args[key]; ok {
|
|
||||||
return strings.TrimSpace(fmt.Sprint(x))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func numArg(req mcp.CallToolRequest, key string) (float64, bool) {
|
|
||||||
if args, ok := req.Params.Arguments.(map[string]any); ok {
|
|
||||||
if x, ok := args[key]; ok {
|
|
||||||
switch t := x.(type) {
|
|
||||||
case float64:
|
|
||||||
return t, true
|
|
||||||
case int:
|
|
||||||
return float64(t), true
|
|
||||||
case string:
|
|
||||||
if t == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
f, err := strconv.ParseFloat(t, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return f, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func numArgStr(req mcp.CallToolRequest, key, def string) string {
|
|
||||||
if n, ok := numArg(req, key); ok {
|
|
||||||
return strconv.FormatInt(int64(n), 10)
|
|
||||||
}
|
|
||||||
if s := strArg(req, key); s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
func setStr(m map[string]any, k, v string) {
|
|
||||||
if strings.TrimSpace(v) != "" {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstNonEmpty(vals ...string) string {
|
|
||||||
for _, v := range vals {
|
|
||||||
if strings.TrimSpace(v) != "" {
|
|
||||||
return strings.TrimSpace(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskToken(t string) string {
|
func maskToken(t string) string {
|
||||||
t = strings.TrimSpace(t)
|
t = strings.TrimSpace(t)
|
||||||
if t == "" {
|
if t == "" {
|
||||||
@@ -675,3 +103,51 @@ func maskToken(t string) string {
|
|||||||
}
|
}
|
||||||
return t[:4] + "****" + t[len(t)-4:]
|
return t[:4] + "****" + t[len(t)-4:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Fprintf(os.Stderr, `
|
||||||
|
TaskPool MCP Server(内置模式)
|
||||||
|
|
||||||
|
MCP Server 已集成到后端服务中,推荐直接使用内置端点:
|
||||||
|
http://your-server:8052/mcp
|
||||||
|
|
||||||
|
此命令用于:
|
||||||
|
- stdio 模式:本地 Agent(Claude Desktop / Cursor)
|
||||||
|
- 独立 HTTP 服务:需要单独端口时
|
||||||
|
|
||||||
|
用法:
|
||||||
|
taskpool mcp # stdio 模式
|
||||||
|
taskpool mcp --http # HTTP 模式,监听 :8053
|
||||||
|
taskpool mcp --http :9000 # HTTP 模式,指定端口
|
||||||
|
|
||||||
|
环境变量(stdio 模式可选):
|
||||||
|
TASKPOOL_URL 面板地址(默认本机)
|
||||||
|
TASKPOOL_TOKEN OpenAPI Token(默认从系统设置读取)
|
||||||
|
|
||||||
|
Agent 配置示例:
|
||||||
|
HTTP 模式(推荐):
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"taskpool": {
|
||||||
|
"url": "http://your-server:8052/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer 你的Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdio 模式:
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"taskpool": {
|
||||||
|
"command": "taskpool",
|
||||||
|
"args": ["mcp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
前置条件:
|
||||||
|
系统设置 → 启用 OpenAPI → 生成 Token
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|||||||
+27
-8
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,18 +22,36 @@ var (
|
|||||||
openAPIClientOnce sync.Once
|
openAPIClientOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetOpenAPIClient 获取内部 OpenAPI 客户端(单例)
|
// GetOpenAPIClient 获取 OpenAPI 客户端(单例)
|
||||||
// 使用当前服务器的地址和系统设置中的 OpenAPI Token
|
// 优先使用环境变量 TASKPOOL_URL 和 TASKPOOL_TOKEN
|
||||||
|
// 否则使用本地服务器地址和系统设置中的 OpenAPI Token
|
||||||
func GetOpenAPIClient() *Client {
|
func GetOpenAPIClient() *Client {
|
||||||
openAPIClientOnce.Do(func() {
|
openAPIClientOnce.Do(func() {
|
||||||
cfg := services.GetConfig()
|
// 优先从环境变量读取(支持 stdio 模式连接远程服务器)
|
||||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", cfg.Server.Port)
|
envURL := strings.TrimSpace(os.Getenv("TASKPOOL_URL"))
|
||||||
if cfg.Server.URLPrefix != "" {
|
envToken := strings.TrimSpace(os.Getenv("TASKPOOL_TOKEN"))
|
||||||
baseURL += cfg.Server.URLPrefix
|
if envURL == "" {
|
||||||
|
envURL = strings.TrimSpace(os.Getenv("BAIHU_URL"))
|
||||||
|
}
|
||||||
|
if envToken == "" {
|
||||||
|
envToken = strings.TrimSpace(os.Getenv("BAIHU_TOKEN"))
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsSvc := services.NewSettingsService()
|
var baseURL, token string
|
||||||
token := getOpenAPIToken(settingsSvc)
|
if envURL != "" && envToken != "" {
|
||||||
|
// 使用环境变量配置
|
||||||
|
baseURL = envURL
|
||||||
|
token = envToken
|
||||||
|
} else {
|
||||||
|
// 使用本地服务器配置
|
||||||
|
cfg := services.GetConfig()
|
||||||
|
baseURL = fmt.Sprintf("http://127.0.0.1:%d", cfg.Server.Port)
|
||||||
|
if cfg.Server.URLPrefix != "" {
|
||||||
|
baseURL += cfg.Server.URLPrefix
|
||||||
|
}
|
||||||
|
settingsSvc := services.NewSettingsService()
|
||||||
|
token = getOpenAPIToken(settingsSvc)
|
||||||
|
}
|
||||||
|
|
||||||
openAPIClient = NewClient(baseURL, token)
|
openAPIClient = NewClient(baseURL, token)
|
||||||
})
|
})
|
||||||
|
|||||||
+22
-3
@@ -19,9 +19,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mcpServer *server.MCPServer
|
mcpServer *server.MCPServer
|
||||||
mcpHTTPServer *server.StreamableHTTPServer
|
mcpHTTPServer *server.StreamableHTTPServer
|
||||||
once sync.Once
|
externalClient *Client // 外部客户端(用于 stdio 模式连接远程服务器)
|
||||||
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetServer 获取 MCP Server 实例(单例)
|
// GetServer 获取 MCP Server 实例(单例)
|
||||||
@@ -42,6 +43,19 @@ func GetServer() *server.MCPServer {
|
|||||||
return mcpServer
|
return mcpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExternalClient 设置外部 OpenAPI 客户端(用于 stdio 模式)
|
||||||
|
func SetExternalClient(c *Client) {
|
||||||
|
externalClient = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClient 获取当前使用的客户端
|
||||||
|
func getClient() *Client {
|
||||||
|
if externalClient != nil {
|
||||||
|
return externalClient
|
||||||
|
}
|
||||||
|
return GetOpenAPIClient()
|
||||||
|
}
|
||||||
|
|
||||||
// GetHTTPHandler 获取 MCP HTTP Handler(用于集成到 Gin)
|
// GetHTTPHandler 获取 MCP HTTP Handler(用于集成到 Gin)
|
||||||
func GetHTTPHandler() http.Handler {
|
func GetHTTPHandler() http.Handler {
|
||||||
s := GetServer()
|
s := GetServer()
|
||||||
@@ -53,6 +67,11 @@ func GetHTTPHandler() http.Handler {
|
|||||||
return mcpHTTPServer
|
return mcpHTTPServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeStdio 启动 stdio 模式的 MCP Server
|
||||||
|
func ServeStdio(s *server.MCPServer) error {
|
||||||
|
return server.ServeStdio(s)
|
||||||
|
}
|
||||||
|
|
||||||
const instructions = `你是 TaskPool(任务池)运维助手。通过 MCP tools 管理任务调度面板。
|
const instructions = `你是 TaskPool(任务池)运维助手。通过 MCP tools 管理任务调度面板。
|
||||||
|
|
||||||
核心能力:
|
核心能力:
|
||||||
|
|||||||
Reference in New Issue
Block a user