// Package middleware contains MCP tool-handler middlewares. package middleware import ( "context" "time" mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" "gitea.com/gitea/gitea-mcp/pkg/debug" "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/log" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "go.uber.org/zap" ) // ToolLogging wraps a ToolHandlerFunc to emit structured entry/exit logs // for every tool invocation. When debug is off it is effectively a no-op // aside from attaching a request ID and tool name to the context (which the // `to` package and the HTTP RoundTripper read for correlation). // // At debug level we log: // - on entry: tool name, redacted args, request_id, token source // - on exit: status (ok/error), duration_ms, error message if any // // At error level we always log a contextual error entry (even when debug is // off) so an operator looking at the log can tell *which* tool failed. func ToolLogging(next server.ToolHandlerFunc) server.ToolHandlerFunc { return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { reqID := debug.NewRequestID() ctx = context.WithValue(ctx, mcpContext.RequestIDContextKey, reqID) ctx = context.WithValue(ctx, mcpContext.ToolNameContextKey, req.Params.Name) tokenSource := tokenSourceFromContext(ctx) if flag.DebugScopeEnabled("tools") { log.Debug("tool call: start", zap.String("tool", req.Params.Name), zap.String("request_id", reqID), zap.String("token_source", tokenSource), zap.Any("args", debug.RedactArgs(req.Params.Arguments)), ) } start := time.Now() result, err := next(ctx, req) duration := time.Since(start) fields := []zap.Field{ zap.String("tool", req.Params.Name), zap.String("request_id", reqID), zap.Int64("duration_ms", duration.Milliseconds()), } switch { case err != nil: // Always-on contextual error log. This replaces the bare // `log.Errorf("%s", err)` previously in pkg/to.ErrorResult, // which had no tool name attached. log.Error("tool call: error", append(fields, zap.String("error", err.Error()))...) case result != nil && result.IsError: if flag.DebugScopeEnabled("tools") { log.Debug("tool call: returned isError=true", fields...) } default: if flag.DebugScopeEnabled("tools") { log.Debug("tool call: ok", fields...) } } return result, err } } func tokenSourceFromContext(ctx context.Context) string { if ctx != nil { if v, ok := ctx.Value(mcpContext.TokenSourceContextKey).(string); ok && v != "" { return v } } if flag.TokenSource != "" { return flag.TokenSource } return "none" }