mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(mcp): log tool call (function name + arguments)
Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
_output/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.docusaurus/
|
.docusaurus/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user