middleware.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package kit
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "runtime/debug"
  7. "strings"
  8. "sync/atomic"
  9. "time"
  10. "github.com/mark3labs/mcp-go/mcp"
  11. "github.com/mark3labs/mcp-go/server"
  12. "go.uber.org/zap"
  13. )
  14. const (
  15. // DefaultTimeout 普通工具单次调用超时
  16. DefaultTimeout = 60 * time.Second
  17. // ExportTimeout 导出工具单次调用超时
  18. ExportTimeout = 300 * time.Second
  19. // QueueWait 并发已满时的最长排队时间
  20. QueueWait = 30 * time.Second
  21. maxParamsLogBytes = 2 * 1024
  22. )
  23. var callSeq int64
  24. // Middleware 统一的工具调用中间件:并发上限、超时、panic 恢复、调用日志(不含调用人)。
  25. //
  26. // 数据访问函数大多不接收 context,超时后无法真正中止上游请求:处理函数在后台继续执行,
  27. // 直到返回才释放并发名额,从而保证对 FWH 平台和数据库的并发压力不超过上限。
  28. func Middleware(log *zap.Logger, maxConcurrency int) server.ToolHandlerMiddleware {
  29. if maxConcurrency <= 0 {
  30. maxConcurrency = 8
  31. }
  32. sem := make(chan struct{}, maxConcurrency)
  33. return func(next server.ToolHandlerFunc) server.ToolHandlerFunc {
  34. return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  35. name := req.Params.Name
  36. seq := atomic.AddInt64(&callSeq, 1)
  37. started := time.Now()
  38. fields := []zap.Field{zap.Int64("seq", seq), zap.String("tool", name)}
  39. // 1. 并发名额
  40. waitCtx, cancelWait := context.WithTimeout(ctx, QueueWait)
  41. select {
  42. case sem <- struct{}{}:
  43. cancelWait()
  44. case <-waitCtx.Done():
  45. cancelWait()
  46. log.Warn("[agent-mcp] 排队超时", fields...)
  47. return mcp.NewToolResultError("服务繁忙,请稍后重试。"), nil
  48. }
  49. timeout := DefaultTimeout
  50. if strings.HasSuffix(name, "_export") {
  51. timeout = ExportTimeout
  52. }
  53. callCtx, cancel := context.WithTimeout(ctx, timeout)
  54. defer cancel()
  55. log.Info("[agent-mcp] 调用", append(fields, zap.String("params", paramsSummary(req)))...)
  56. type outcome struct {
  57. res *mcp.CallToolResult
  58. err error
  59. }
  60. done := make(chan outcome, 1)
  61. go func() {
  62. defer func() { <-sem }()
  63. defer func() {
  64. if r := recover(); r != nil {
  65. log.Error("[agent-mcp] panic", append(fields, zap.Any("panic", r), zap.String("stack", string(debug.Stack())))...)
  66. done <- outcome{res: mcp.NewToolResultError("内部错误,请稍后重试。")}
  67. }
  68. }()
  69. res, err := next(callCtx, req)
  70. done <- outcome{res: res, err: err}
  71. }()
  72. select {
  73. case o := <-done:
  74. size, isErr := resultInfo(o.res)
  75. l := log.Info
  76. if o.err != nil {
  77. l = log.Error
  78. } else if isErr {
  79. l = log.Warn
  80. }
  81. l("[agent-mcp] 完成", append(fields,
  82. zap.Int64("duration_ms", time.Since(started).Milliseconds()),
  83. zap.Int("result_size", size),
  84. zap.Bool("is_error", isErr),
  85. zap.Error(o.err),
  86. )...)
  87. if o.err != nil {
  88. // 不把 Go error 透传为协议错误,统一给模型一条可读信息
  89. return mcp.NewToolResultError(fmt.Sprintf("工具执行失败:%v", o.err)), nil
  90. }
  91. return o.res, nil
  92. case <-callCtx.Done():
  93. log.Warn("[agent-mcp] 超时", append(fields, zap.Duration("timeout", timeout))...)
  94. return mcp.NewToolResultError(fmt.Sprintf("查询超时(%d 秒),请缩小时间范围或稍后重试。", int(timeout.Seconds()))), nil
  95. }
  96. }
  97. }
  98. }
  99. func paramsSummary(req mcp.CallToolRequest) string {
  100. b, err := json.Marshal(req.GetArguments())
  101. if err != nil {
  102. return ""
  103. }
  104. if len(b) > maxParamsLogBytes {
  105. return string(b[:maxParamsLogBytes]) + "...(truncated)"
  106. }
  107. return string(b)
  108. }
  109. func resultInfo(r *mcp.CallToolResult) (size int, isErr bool) {
  110. if r == nil {
  111. return 0, false
  112. }
  113. for _, c := range r.Content {
  114. switch t := c.(type) {
  115. case mcp.TextContent:
  116. size += len(t.Text)
  117. case mcp.EmbeddedResource:
  118. if tr, ok := t.Resource.(mcp.TextResourceContents); ok {
  119. size += len(tr.Text)
  120. }
  121. }
  122. }
  123. return size, r.IsError
  124. }