| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- package kit
- import (
- "context"
- "encoding/json"
- "fmt"
- "runtime/debug"
- "strings"
- "sync/atomic"
- "time"
- "github.com/mark3labs/mcp-go/mcp"
- "github.com/mark3labs/mcp-go/server"
- "go.uber.org/zap"
- )
- const (
- // DefaultTimeout 普通工具单次调用超时
- DefaultTimeout = 60 * time.Second
- // ExportTimeout 导出工具单次调用超时
- ExportTimeout = 300 * time.Second
- // QueueWait 并发已满时的最长排队时间
- QueueWait = 30 * time.Second
- maxParamsLogBytes = 2 * 1024
- )
- var callSeq int64
- // Middleware 统一的工具调用中间件:并发上限、超时、panic 恢复、调用日志(不含调用人)。
- //
- // 数据访问函数大多不接收 context,超时后无法真正中止上游请求:处理函数在后台继续执行,
- // 直到返回才释放并发名额,从而保证对 FWH 平台和数据库的并发压力不超过上限。
- func Middleware(log *zap.Logger, maxConcurrency int) server.ToolHandlerMiddleware {
- if maxConcurrency <= 0 {
- maxConcurrency = 8
- }
- sem := make(chan struct{}, maxConcurrency)
- return func(next server.ToolHandlerFunc) server.ToolHandlerFunc {
- return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- name := req.Params.Name
- seq := atomic.AddInt64(&callSeq, 1)
- started := time.Now()
- fields := []zap.Field{zap.Int64("seq", seq), zap.String("tool", name)}
- // 1. 并发名额
- waitCtx, cancelWait := context.WithTimeout(ctx, QueueWait)
- select {
- case sem <- struct{}{}:
- cancelWait()
- case <-waitCtx.Done():
- cancelWait()
- log.Warn("[agent-mcp] 排队超时", fields...)
- return mcp.NewToolResultError("服务繁忙,请稍后重试。"), nil
- }
- timeout := DefaultTimeout
- if strings.HasSuffix(name, "_export") {
- timeout = ExportTimeout
- }
- callCtx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
- log.Info("[agent-mcp] 调用", append(fields, zap.String("params", paramsSummary(req)))...)
- type outcome struct {
- res *mcp.CallToolResult
- err error
- }
- done := make(chan outcome, 1)
- go func() {
- defer func() { <-sem }()
- defer func() {
- if r := recover(); r != nil {
- log.Error("[agent-mcp] panic", append(fields, zap.Any("panic", r), zap.String("stack", string(debug.Stack())))...)
- done <- outcome{res: mcp.NewToolResultError("内部错误,请稍后重试。")}
- }
- }()
- res, err := next(callCtx, req)
- done <- outcome{res: res, err: err}
- }()
- select {
- case o := <-done:
- size, isErr := resultInfo(o.res)
- l := log.Info
- if o.err != nil {
- l = log.Error
- } else if isErr {
- l = log.Warn
- }
- l("[agent-mcp] 完成", append(fields,
- zap.Int64("duration_ms", time.Since(started).Milliseconds()),
- zap.Int("result_size", size),
- zap.Bool("is_error", isErr),
- zap.Error(o.err),
- )...)
- if o.err != nil {
- // 不把 Go error 透传为协议错误,统一给模型一条可读信息
- return mcp.NewToolResultError(fmt.Sprintf("工具执行失败:%v", o.err)), nil
- }
- return o.res, nil
- case <-callCtx.Done():
- log.Warn("[agent-mcp] 超时", append(fields, zap.Duration("timeout", timeout))...)
- return mcp.NewToolResultError(fmt.Sprintf("查询超时(%d 秒),请缩小时间范围或稍后重试。", int(timeout.Seconds()))), nil
- }
- }
- }
- }
- func paramsSummary(req mcp.CallToolRequest) string {
- b, err := json.Marshal(req.GetArguments())
- if err != nil {
- return ""
- }
- if len(b) > maxParamsLogBytes {
- return string(b[:maxParamsLogBytes]) + "...(truncated)"
- }
- return string(b)
- }
- func resultInfo(r *mcp.CallToolResult) (size int, isErr bool) {
- if r == nil {
- return 0, false
- }
- for _, c := range r.Content {
- switch t := c.(type) {
- case mcp.TextContent:
- size += len(t.Text)
- case mcp.EmbeddedResource:
- if tr, ok := t.Resource.(mcp.TextResourceContents); ok {
- size += len(tr.Text)
- }
- }
- }
- return size, r.IsError
- }
|