139 lines
4.5 KiB
Go
139 lines
4.5 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewServerToolWithContextHandler_InvalidArguments_ReturnsIsError(t *testing.T) {
|
|
type expectedArgs struct {
|
|
Query string `json:"query"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
|
|
tool := NewServerToolWithContextHandler(
|
|
mcp.Tool{Name: "test_context_tool"},
|
|
testToolsetMetadata("test"),
|
|
func(_ context.Context, _ *mcp.CallToolRequest, _ expectedArgs) (*mcp.CallToolResult, any, error) {
|
|
t.Fatal("handler should not be called with invalid arguments")
|
|
return nil, nil, nil
|
|
},
|
|
)
|
|
|
|
handler := tool.HandlerFunc(nil)
|
|
|
|
result, err := handler(context.Background(), &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Name: "test_context_tool",
|
|
Arguments: json.RawMessage(`{not valid json`),
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.IsError)
|
|
assert.Len(t, result.Content, 1)
|
|
textContent, ok := result.Content[0].(*mcp.TextContent)
|
|
require.True(t, ok)
|
|
assert.Contains(t, textContent.Text, "invalid arguments")
|
|
}
|
|
|
|
func TestNewServerToolWithContextHandler_ValidArguments_Succeeds(t *testing.T) {
|
|
type expectedArgs struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
}
|
|
|
|
tool := NewServerToolWithContextHandler(
|
|
mcp.Tool{Name: "test_tool"},
|
|
testToolsetMetadata("test"),
|
|
func(_ context.Context, _ *mcp.CallToolRequest, args expectedArgs) (*mcp.CallToolResult, any, error) {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "success: " + args.Owner + "/" + args.Repo},
|
|
},
|
|
}, nil, nil
|
|
},
|
|
)
|
|
|
|
handler := tool.HandlerFunc(nil)
|
|
|
|
goodArgs, _ := json.Marshal(map[string]any{"owner": "octocat", "repo": "hello-world"})
|
|
result, err := handler(context.Background(), &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Name: "test_tool",
|
|
Arguments: goodArgs,
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.False(t, result.IsError)
|
|
textContent, ok := result.Content[0].(*mcp.TextContent)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "success: octocat/hello-world", textContent.Text)
|
|
}
|
|
|
|
func TestAnnotateHeaderParams(t *testing.T) {
|
|
tool := &mcp.Tool{InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {Type: "string"},
|
|
"repo": {Type: "string"},
|
|
"detail": {Type: "string"},
|
|
},
|
|
}}
|
|
AnnotateHeaderParams(tool)
|
|
schema := tool.InputSchema.(*jsonschema.Schema)
|
|
assert.Equal(t, "owner", schema.Properties["owner"].Extra["x-mcp-header"])
|
|
assert.Equal(t, "repo", schema.Properties["repo"].Extra["x-mcp-header"])
|
|
assert.Nil(t, schema.Properties["detail"].Extra)
|
|
|
|
// No-op for tools without owner/repo and when InputSchema is not a *jsonschema.Schema
|
|
AnnotateHeaderParams(&mcp.Tool{InputSchema: &jsonschema.Schema{Properties: map[string]*jsonschema.Schema{"x": {}}}})
|
|
AnnotateHeaderParams(&mcp.Tool{InputSchema: json.RawMessage(`{}`)})
|
|
}
|
|
|
|
func TestAnnotateHeaderParams_DoesNotMutateOriginal(t *testing.T) {
|
|
orig := &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{"owner": {Type: "string"}, "repo": {Type: "string"}},
|
|
}
|
|
tool := &mcp.Tool{InputSchema: orig}
|
|
AnnotateHeaderParams(tool)
|
|
|
|
// Original schema and its property Extra maps must be untouched.
|
|
require.Nil(t, orig.Properties["owner"].Extra, "must not mutate original owner schema")
|
|
require.Nil(t, orig.Properties["repo"].Extra, "must not mutate original repo schema")
|
|
// Returned copy carries the annotation.
|
|
got := tool.InputSchema.(*jsonschema.Schema)
|
|
require.NotSame(t, orig, got, "must replace InputSchema with a copy")
|
|
require.Equal(t, "owner", got.Properties["owner"].Extra["x-mcp-header"])
|
|
}
|
|
|
|
func TestAnnotateHeaderParams_ConcurrentRegistrationIsRaceFree(t *testing.T) {
|
|
// Shared base schema, as ServerTool.Tool is shallow-copied per registration.
|
|
base := &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{"owner": {Type: "string"}, "repo": {Type: "string"}},
|
|
}
|
|
var wg sync.WaitGroup
|
|
for range 64 {
|
|
wg.Go(func() {
|
|
tool := mcp.Tool{InputSchema: base} // shallow copy shares *Schema
|
|
AnnotateHeaderParams(&tool)
|
|
got := tool.InputSchema.(*jsonschema.Schema)
|
|
require.Equal(t, "repo", got.Properties["repo"].Extra["x-mcp-header"])
|
|
})
|
|
}
|
|
wg.Wait()
|
|
require.Nil(t, base.Properties["owner"].Extra, "shared base must remain unmutated")
|
|
}
|