Files
artex/server/mcpdiscover.go
T
dela 97668dab7e
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run
Localize user-facing UI, agent output, and backend messages to Simplified Chinese
2026-10-09 14:08:31 +08:00

107 lines
3.7 KiB
Go

package server
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/mcphttp"
"github.com/Autumn-27/norma/mcp"
actool "github.com/Autumn-27/norma/tool"
)
// mcpClient is the shared surface of a connected MCP server (stdio, Streamable HTTP,
// or legacy SSE),
// so tools/list and cleanup are handled uniformly regardless of transport.
type mcpClient interface {
Tools(context.Context) ([]actool.CoreTool, error)
Close() error
}
// connectMCP 이 돌려주는 전송 설정 검증 오류 네 가지는 사용자 노출이다 —
// pgRefreshMCP(server_mgmt.go) 가 discoverAndCacheMCP 실패를
// writeErr(502, errMgmtToolDiscoverFail+err.Error()) 로 응답 본문에 그대로 싣는다.
// 다른 호출처(assembly.go 의 에이전트 조립·discoverEmptyMCPsOnStartup 시작 자동 발견·
// sync_scopesentry.go 동기화·pgSaveMCP 추가 직후 발견)는 전부 log.Printf 로만 남기거나
// 버려서 에이전트 두뇌 입력이 아니다. 그래서 한국어화한다. 전송 방식 enum
// (stdio/http/sse)과 URL 은 와이어 식별자라 원문을 유지하고, UI mcpPage(전송 방식·명령·
// 원격 URL)와 표기를 맞춘다.
const (
errMCPStdioNoCommand = "stdio 传输方式未指定命令"
errMCPHTTPNoURL = "http 传输方式未指定 URL"
errMCPSSENoURL = "sse 传输方式未指定 URL"
errMCPUnknownTransportFmt = "未知的传输方式: %q"
)
// connectMCP dials one MCP server per its transport. Callers must Close the client.
func connectMCP(ctx context.Context, m *db.MCPServer) (mcpClient, error) {
switch m.Transport {
case "stdio":
if m.Command == "" {
return nil, errors.New(errMCPStdioNoCommand)
}
return mcp.NewStdioClient(ctx, m.Name, m.Command, jsonStrMap(m.Env), jsonStrSlice(m.Args)...)
case "http":
if m.URL == "" {
return nil, errors.New(errMCPHTTPNoURL)
}
// env map doubles as HTTP headers (e.g. Authorization).
return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure)
case "sse":
if m.URL == "" {
return nil, errors.New(errMCPSSENoURL)
}
return mcphttp.NewSSE(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure)
default:
return nil, fmt.Errorf(errMCPUnknownTransportFmt, m.Transport)
}
}
// discoverAndCacheMCP connects to one MCP, lists its tools, and persists the tool
// names to mcp_tools_cache so the UI shows them without a live connection.
func (s *Server) discoverAndCacheMCP(ctx context.Context, m *db.MCPServer) error {
cl, err := connectMCP(ctx, m)
if err != nil {
return err
}
defer cl.Close()
ts, err := cl.Tools(ctx)
if err != nil {
return err
}
tools := make([]db.MCPTool, 0, len(ts))
for _, t := range ts {
tools = append(tools, db.MCPTool{Name: t.Name(), Description: t.Description()})
}
if err := s.m.pg.SaveMCPTools(m.ID, tools); err != nil {
return err
}
log.Printf("[mcp] 在 %s 上发现并缓存了 %d 个工具", m.Name, len(tools))
return nil
}
// discoverEmptyMCPsOnStartup fills the tool cache for any enabled MCP that has none
// yet (notably the seeded browser MCP on first run). Runs sequentially in one
// goroutine so we never spawn many stdio servers (npx) at once, and never blocks
// startup. Best-effort: a failure leaves the cache empty to retry next start.
func (s *Server) discoverEmptyMCPsOnStartup() {
servers, err := s.m.pg.ListMCP()
if err != nil {
log.Printf("[mcp] 启动时自动发现: 无法读取列表: %v", err)
return
}
for _, m := range servers {
if !m.Enabled || len(m.Tools) > 0 {
continue
}
ctx, cancel := context.WithTimeout(s.ctx, 90*time.Second)
if err := s.discoverAndCacheMCP(ctx, m); err != nil {
log.Printf("[mcp] 启动时对 %s 的自动发现失败: %v", m.Name, err)
}
cancel()
}
}