feat(mcp): log tool call (function name + arguments)

Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
Marc Nuri
2025-07-22 14:35:19 +02:00
committed by GitHub
parent 3fbfd8d7cb
commit ca0aa4648d
5 changed files with 56 additions and 4 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
_output/
.idea/ .idea/
.vscode/ .vscode/
.docusaurus/ .docusaurus/

View File

@@ -69,7 +69,7 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized) http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized)
return return
} }
if oidcProvider != nil { if oidcProvider != nil {
// If OIDC Provider is configured, this token must be validated against it. // If OIDC Provider is configured, this token must be validated against it.
if err := validateTokenWithOIDC(r.Context(), oidcProvider, token, audience); err != nil { if err := validateTokenWithOIDC(r.Context(), oidcProvider, token, audience); err != nil {

View File

@@ -1,13 +1,18 @@
package mcp package mcp
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"testing" "testing"
"time" "time"
@@ -97,6 +102,7 @@ func TestMain(m *testing.M) {
type mcpContext struct { type mcpContext struct {
profile Profile profile Profile
listOutput output.Output listOutput output.Output
logLevel int
staticConfig *config.StaticConfig staticConfig *config.StaticConfig
clientOptions []transport.ClientOption clientOptions []transport.ClientOption
@@ -108,6 +114,8 @@ type mcpContext struct {
mcpServer *Server mcpServer *Server
mcpHttpServer *httptest.Server mcpHttpServer *httptest.Server
mcpClient *client.Client mcpClient *client.Client
klogState klog.State
logBuffer bytes.Buffer
} }
func (c *mcpContext) beforeEach(t *testing.T) { func (c *mcpContext) beforeEach(t *testing.T) {
@@ -130,6 +138,13 @@ func (c *mcpContext) beforeEach(t *testing.T) {
if c.before != nil { if c.before != nil {
c.before(c) c.before(c)
} }
// Set up logging
c.klogState = klog.CaptureState()
flags := flag.NewFlagSet("test", flag.ContinueOnError)
klog.InitFlags(flags)
_ = flags.Set("v", strconv.Itoa(c.logLevel))
klog.SetLogger(textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(c.logLevel), textlogger.Output(&c.logBuffer))))
// MCP Server
if c.mcpServer, err = NewServer(Configuration{ if c.mcpServer, err = NewServer(Configuration{
Profile: c.profile, Profile: c.profile,
ListOutput: c.listOutput, ListOutput: c.listOutput,
@@ -143,6 +158,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
t.Fatal(err) t.Fatal(err)
return return
} }
// MCP Client
if err = c.mcpClient.Start(c.ctx); err != nil { if err = c.mcpClient.Start(c.ctx); err != nil {
t.Fatal(err) t.Fatal(err)
return return
@@ -165,6 +181,7 @@ func (c *mcpContext) afterEach() {
c.mcpServer.Close() c.mcpServer.Close()
_ = c.mcpClient.Close() _ = c.mcpClient.Close()
c.mcpHttpServer.Close() c.mcpHttpServer.Close()
c.klogState.Restore()
} }
func testCase(t *testing.T, test func(c *mcpContext)) { func testCase(t *testing.T, test func(c *mcpContext)) {

View File

@@ -3,6 +3,7 @@ package mcp
import ( import (
"context" "context"
"fmt" "fmt"
"k8s.io/klog/v2"
"net/http" "net/http"
"slices" "slices"
@@ -56,6 +57,7 @@ func NewServer(configuration Configuration) (*Server, error) {
server.WithPromptCapabilities(true), server.WithPromptCapabilities(true),
server.WithToolCapabilities(true), server.WithToolCapabilities(true),
server.WithLogging(), server.WithLogging(),
server.WithToolHandlerMiddleware(toolCallLoggingMiddleware),
), ),
} }
if err := s.reloadKubernetesClient(); err != nil { if err := s.reloadKubernetesClient(); err != nil {
@@ -165,3 +167,10 @@ func contextFunc(ctx context.Context, r *http.Request) context.Context {
return ctx return ctx
} }
func toolCallLoggingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
klog.V(5).Infof("mcp tool call: %s(%v)", ctr.Params.Name, ctr.Params.Arguments)
return next(ctx, ctr)
}
}

View File

@@ -1,10 +1,11 @@
package mcp package mcp
import ( import (
"k8s.io/utils/ptr"
"testing"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"regexp"
"strings"
"testing"
"github.com/manusa/kubernetes-mcp-server/pkg/config" "github.com/manusa/kubernetes-mcp-server/pkg/config"
) )
@@ -116,3 +117,27 @@ func TestDisabledTools(t *testing.T) {
}) })
}) })
} }
func TestToolCallLogging(t *testing.T) {
testCaseWithContext(t, &mcpContext{logLevel: 5}, func(c *mcpContext) {
_, _ = c.callTool("configuration_view", map[string]interface{}{
"minified": false,
})
t.Run("Logs tool name", func(t *testing.T) {
expectedLog := "mcp tool call: configuration_view("
if !strings.Contains(c.logBuffer.String(), expectedLog) {
t.Errorf("Expected log to contain '%s', got: %s", expectedLog, c.logBuffer.String())
}
})
t.Run("Logs tool call arguments", func(t *testing.T) {
expected := `"mcp tool call: configuration_view\((.+)\)"`
m := regexp.MustCompile(expected).FindStringSubmatch(c.logBuffer.String())
if len(m) != 2 {
t.Fatalf("Expected log entry to contain arguments, got %s", c.logBuffer.String())
}
if m[1] != "map[minified:false]" {
t.Errorf("Expected log arguments to be 'map[minified:false]', got %s", m[1])
}
})
})
}