Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion go/core/internal/mcp/mcp_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"
"time"

"github.com/google/jsonschema-go/jsonschema"
"github.com/kagent-dev/kagent/go/api/v1alpha2"
"github.com/kagent-dev/kagent/go/core/internal/a2a"
authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth"
Expand Down Expand Up @@ -81,12 +82,21 @@ func NewMCPHandler(kubeClient client.Client, a2aBaseURL string, authenticator au
server := mcpsdk.NewServer(impl, nil)
handler.server = server

// Add list_agents tool
// Add list_agents tool.
// InputSchema is set explicitly (rather than reflected from the empty
// ListAgentsInput struct) so the serialized schema includes "properties": {}.
// OpenAI strict mode rejects object schemas without a properties key.
// See https://github.com/kagent-dev/kagent/issues/1889.
mcpsdk.AddTool[ListAgentsInput, ListAgentsOutput](
server,
&mcpsdk.Tool{
Name: "list_agents",
Description: "List invokable kagent agents (accepted + deploymentReady)",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
},
handler.handleListAgents,
)
Expand Down
76 changes: 76 additions & 0 deletions go/core/internal/mcp/mcp_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package mcp

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/kagent-dev/kagent/go/api/v1alpha2"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

// TestListAgentsInputSchemaHasProperties asserts that the list_agents tool
// advertises an inputSchema containing an explicit "properties" key, even
// though it accepts no arguments. OpenAI strict mode requires this.
// Regression test for https://github.com/kagent-dev/kagent/issues/1889.
func TestListAgentsInputSchemaHasProperties(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, v1alpha2.AddToScheme(scheme))
kubeClient := fake.NewClientBuilder().WithScheme(scheme).Build()

h, err := NewMCPHandler(kubeClient, "http://unused", nil, time.Minute)
require.NoError(t, err)

clientTransport, serverTransport := mcpsdk.NewInMemoryTransports()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

// Run the server in a goroutine; it returns when the transport closes.
serverDone := make(chan error, 1)
go func() {
serverDone <- h.server.Run(ctx, serverTransport)
}()
// Registered first so it runs last (LIFO): after session.Close below has
// disconnected the client, cancel the context and drain the server's
// return value so the goroutine cannot leak and unexpected errors surface.
t.Cleanup(func() {
cancel()
if err := <-serverDone; err != nil && err != context.Canceled {
t.Errorf("MCP server returned unexpected error: %v", err)
}
})
Comment thread
pboers1988 marked this conversation as resolved.

client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "test", Version: "0.0.0"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
require.NoError(t, err)
t.Cleanup(func() { session.Close() })

tools, err := session.ListTools(ctx, &mcpsdk.ListToolsParams{})
require.NoError(t, err)

var listAgents *mcpsdk.Tool
for i := range tools.Tools {
if tools.Tools[i].Name == "list_agents" {
listAgents = tools.Tools[i]
break
}
}
require.NotNil(t, listAgents, "list_agents tool not registered")

raw, err := json.Marshal(listAgents.InputSchema)
require.NoError(t, err)

var schema map[string]any
require.NoError(t, json.Unmarshal(raw, &schema))

require.Equal(t, "object", schema["type"], "inputSchema type must be object")
props, ok := schema["properties"]
require.True(t, ok, "inputSchema must include a properties key (got %s)", string(raw))
require.IsType(t, map[string]any{}, props, "properties must be a JSON object")
require.Empty(t, props, "list_agents takes no args, properties should be empty")
require.Equal(t, false, schema["additionalProperties"], "additionalProperties must remain false")
}
Loading