feat(mcp): integrate MCP Server into backend service as built-in endpoint
Build and Deploy / build-and-push (push) Successful in 54s

- Add MCP Controller and register /mcp route
- MCP Server now uses OpenAPI Token for authentication
- Update frontend settings to show built-in MCP endpoint config
- Update docs to reflect integrated MCP endpoint
This commit is contained in:
2026-07-27 02:38:09 +08:00
parent f004cfc937
commit 066383de83
8 changed files with 870 additions and 31 deletions
+191
View File
@@ -0,0 +1,191 @@
package mcp
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/engigu/taskpool/internal/constant"
"github.com/engigu/taskpool/internal/models/vo"
"github.com/engigu/taskpool/internal/services"
)
var (
openAPIClient *Client
openAPIClientOnce sync.Once
)
// GetOpenAPIClient 获取内部 OpenAPI 客户端(单例)
// 使用当前服务器的地址和系统设置中的 OpenAPI Token
func GetOpenAPIClient() *Client {
openAPIClientOnce.Do(func() {
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)
})
return openAPIClient
}
// RefreshClient 刷新客户端(当 OpenAPI Token 变更时调用)
func RefreshClient() {
settingsSvc := services.NewSettingsService()
token := getOpenAPIToken(settingsSvc)
if openAPIClient != nil {
openAPIClient.Token = token
}
}
func getOpenAPIToken(settingsSvc *services.SettingsService) string {
siteConfig := settingsSvc.GetSection(constant.SectionSite)
tokenJson, ok := siteConfig[constant.KeyOpenapiToken]
if !ok || tokenJson == "" {
return ""
}
var tokenConfig vo.TokenConfig
if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err != nil {
return ""
}
if !tokenConfig.Enabled {
return ""
}
return tokenConfig.Token
}
// Client 调用内部 OpenAPI
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("服务器地址未配置")
}
if c.Token == "" {
return nil, fmt.Errorf("OpenAPI Token 未配置,请在系统设置中启用 OpenAPI 并生成 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
}
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
}
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 truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}