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/
.vscode/
.docusaurus/

View File

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

View File

@@ -3,6 +3,7 @@ package mcp
import (
"context"
"fmt"
"k8s.io/klog/v2"
"net/http"
"slices"
@@ -56,6 +57,7 @@ func NewServer(configuration Configuration) (*Server, error) {
server.WithPromptCapabilities(true),
server.WithToolCapabilities(true),
server.WithLogging(),
server.WithToolHandlerMiddleware(toolCallLoggingMiddleware),
),
}
if err := s.reloadKubernetesClient(); err != nil {
@@ -165,3 +167,10 @@ func contextFunc(ctx context.Context, r *http.Request) context.Context {
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
import (
"k8s.io/utils/ptr"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"regexp"
"strings"
"testing"
"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])
}
})
})
}