chore(tests): add new tests for tool registration and resource templates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,4 +29,5 @@ go.work.sum
|
||||
configs/*.local.yaml
|
||||
cmd/amcs-server/__debug_*
|
||||
bin/
|
||||
.cache/
|
||||
OB1/
|
||||
28
Makefile
28
Makefile
@@ -1,7 +1,9 @@
|
||||
BIN_DIR := bin
|
||||
GO_CACHE_DIR := $(CURDIR)/.cache/go-build
|
||||
SERVER_BIN := $(BIN_DIR)/amcs-server
|
||||
CMD_SERVER := ./cmd/amcs-server
|
||||
BUILDINFO_PKG := git.warky.dev/wdevs/amcs/internal/buildinfo
|
||||
PATCH_INCREMENT ?= 1
|
||||
VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
|
||||
COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
@@ -11,7 +13,7 @@ LDFLAGS := -s -w \
|
||||
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
|
||||
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
|
||||
|
||||
.PHONY: all build clean migrate
|
||||
.PHONY: all build clean migrate release-version test
|
||||
|
||||
all: build
|
||||
|
||||
@@ -19,6 +21,30 @@ build:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER)
|
||||
|
||||
test:
|
||||
@mkdir -p $(GO_CACHE_DIR)
|
||||
GOCACHE=$(GO_CACHE_DIR) go test ./...
|
||||
|
||||
release-version:
|
||||
@case "$(PATCH_INCREMENT)" in \
|
||||
''|*[!0-9]*|0) echo "PATCH_INCREMENT must be a positive integer" >&2; exit 1 ;; \
|
||||
esac
|
||||
@latest=$$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1); \
|
||||
if [ -z "$$latest" ]; then latest="v0.0.0"; fi; \
|
||||
version=$${latest#v}; \
|
||||
major=$${version%%.*}; \
|
||||
rest=$${version#*.}; \
|
||||
minor=$${rest%%.*}; \
|
||||
patch=$${rest##*.}; \
|
||||
next_patch=$$((patch + $(PATCH_INCREMENT))); \
|
||||
next_tag="v$$major.$$minor.$$next_patch"; \
|
||||
if git rev-parse -q --verify "refs/tags/$$next_tag" >/dev/null; then \
|
||||
echo "$$next_tag already exists" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
git tag -a "$$next_tag" -m "Release $$next_tag"; \
|
||||
echo "$$next_tag"
|
||||
|
||||
migrate:
|
||||
./scripts/migrate.sh
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package mcpserver
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -99,3 +100,130 @@ func TestAddToolReturnsSchemaErrorInsteadOfPanicking(t *testing.T) {
|
||||
t.Fatalf("addTool() error = %q, want tool context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToolAppliesInputDefaultsAndSetsStructuredContent(t *testing.T) {
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil)
|
||||
|
||||
type helloInput struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
type helloOutput struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
tool := &mcp.Tool{
|
||||
Name: "hello",
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Default: json.RawMessage(`"world"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var gotInput helloInput
|
||||
if err := addTool(server, nil, tool, func(_ context.Context, _ *mcp.CallToolRequest, in helloInput) (*mcp.CallToolResult, helloOutput, error) {
|
||||
gotInput = in
|
||||
return nil, helloOutput{Message: "hello " + in.Name}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("addTool() error = %v", err)
|
||||
}
|
||||
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
_, err := server.Connect(context.Background(), st, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect server: %v", err)
|
||||
}
|
||||
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "0.0.1"}, nil)
|
||||
cs, err := client.Connect(context.Background(), ct, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect client: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cs.Close()
|
||||
}()
|
||||
|
||||
result, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: "hello",
|
||||
Arguments: map[string]any{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CallTool(hello) error = %v", err)
|
||||
}
|
||||
|
||||
if gotInput.Name != "world" {
|
||||
t.Fatalf("handler input name = %q, want %q", gotInput.Name, "world")
|
||||
}
|
||||
|
||||
gotStructured, ok := result.StructuredContent.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("structured content type = %T, want map[string]any", result.StructuredContent)
|
||||
}
|
||||
if gotStructured["message"] != "hello world" {
|
||||
t.Fatalf("structured content message = %#v, want %q", gotStructured["message"], "hello world")
|
||||
}
|
||||
|
||||
if len(result.Content) != 1 {
|
||||
t.Fatalf("content count = %d, want 1", len(result.Content))
|
||||
}
|
||||
|
||||
textContent, ok := result.Content[0].(*mcp.TextContent)
|
||||
if !ok {
|
||||
t.Fatalf("content[0] type = %T, want *mcp.TextContent", result.Content[0])
|
||||
}
|
||||
if textContent.Text != `{"message":"hello world"}` {
|
||||
t.Fatalf("content[0].Text = %q, want %q", textContent.Text, `{"message":"hello world"}`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToolWrapsRegularErrorsInToolResults(t *testing.T) {
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil)
|
||||
|
||||
toolErr := errors.New("boom")
|
||||
if err := addTool(server, nil, &mcp.Tool{Name: "explode"}, func(_ context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, struct{}, error) {
|
||||
return nil, struct{}{}, toolErr
|
||||
}); err != nil {
|
||||
t.Fatalf("addTool() error = %v", err)
|
||||
}
|
||||
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
_, err := server.Connect(context.Background(), st, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect server: %v", err)
|
||||
}
|
||||
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "0.0.1"}, nil)
|
||||
cs, err := client.Connect(context.Background(), ct, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect client: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cs.Close()
|
||||
}()
|
||||
|
||||
result, err := cs.CallTool(context.Background(), &mcp.CallToolParams{Name: "explode"})
|
||||
if err != nil {
|
||||
t.Fatalf("CallTool(explode) error = %v, want nil transport error", err)
|
||||
}
|
||||
if !result.IsError {
|
||||
t.Fatal("CallTool(explode) IsError = false, want true")
|
||||
}
|
||||
if result.StructuredContent != nil {
|
||||
t.Fatalf("structured content = %#v, want nil", result.StructuredContent)
|
||||
}
|
||||
if len(result.Content) != 1 {
|
||||
t.Fatalf("content count = %d, want 1", len(result.Content))
|
||||
}
|
||||
|
||||
textContent, ok := result.Content[0].(*mcp.TextContent)
|
||||
if !ok {
|
||||
t.Fatalf("content[0] type = %T, want *mcp.TextContent", result.Content[0])
|
||||
}
|
||||
if textContent.Text != toolErr.Error() {
|
||||
t.Fatalf("content[0].Text = %q, want %q", textContent.Text, toolErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
151
internal/mcpserver/server_test.go
Normal file
151
internal/mcpserver/server_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
)
|
||||
|
||||
func TestNewListsAllRegisteredTools(t *testing.T) {
|
||||
cs := newStreamableTestClient(t)
|
||||
|
||||
result, err := cs.ListTools(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ListTools() error = %v", err)
|
||||
}
|
||||
|
||||
got := make([]string, 0, len(result.Tools))
|
||||
for _, tool := range result.Tools {
|
||||
got = append(got, tool.Name)
|
||||
}
|
||||
sort.Strings(got)
|
||||
|
||||
want := []string{
|
||||
"add_activity",
|
||||
"add_family_member",
|
||||
"add_guardrail",
|
||||
"add_household_item",
|
||||
"add_important_date",
|
||||
"add_maintenance_task",
|
||||
"add_professional_contact",
|
||||
"add_project_guardrail",
|
||||
"add_project_skill",
|
||||
"add_recipe",
|
||||
"add_skill",
|
||||
"add_vendor",
|
||||
"archive_thought",
|
||||
"backfill_embeddings",
|
||||
"capture_thought",
|
||||
"create_meal_plan",
|
||||
"create_opportunity",
|
||||
"create_project",
|
||||
"delete_thought",
|
||||
"generate_shopping_list",
|
||||
"get_active_project",
|
||||
"get_contact_history",
|
||||
"get_follow_ups_due",
|
||||
"get_household_item",
|
||||
"get_meal_plan",
|
||||
"get_project_context",
|
||||
"get_thought",
|
||||
"get_upcoming_dates",
|
||||
"get_upcoming_maintenance",
|
||||
"get_version_info",
|
||||
"get_week_schedule",
|
||||
"link_thought_to_contact",
|
||||
"link_thoughts",
|
||||
"list_family_members",
|
||||
"list_files",
|
||||
"list_guardrails",
|
||||
"list_project_guardrails",
|
||||
"list_project_skills",
|
||||
"list_projects",
|
||||
"list_skills",
|
||||
"list_thoughts",
|
||||
"list_vendors",
|
||||
"load_file",
|
||||
"log_interaction",
|
||||
"log_maintenance",
|
||||
"recall_context",
|
||||
"related_thoughts",
|
||||
"remove_guardrail",
|
||||
"remove_project_guardrail",
|
||||
"remove_project_skill",
|
||||
"remove_skill",
|
||||
"reparse_thought_metadata",
|
||||
"retry_failed_metadata",
|
||||
"save_file",
|
||||
"search_activities",
|
||||
"search_contacts",
|
||||
"search_household_items",
|
||||
"search_maintenance_history",
|
||||
"search_recipes",
|
||||
"search_thoughts",
|
||||
"set_active_project",
|
||||
"summarize_thoughts",
|
||||
"thought_stats",
|
||||
"update_recipe",
|
||||
"update_thought",
|
||||
"upload_file",
|
||||
}
|
||||
sort.Strings(want)
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ListTools() names = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewListsStoredFileResourceTemplate(t *testing.T) {
|
||||
cs := newStreamableTestClient(t)
|
||||
|
||||
result, err := cs.ListResourceTemplates(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ListResourceTemplates() error = %v", err)
|
||||
}
|
||||
|
||||
if len(result.ResourceTemplates) != 1 {
|
||||
t.Fatalf("ListResourceTemplates() count = %d, want 1", len(result.ResourceTemplates))
|
||||
}
|
||||
|
||||
template := result.ResourceTemplates[0]
|
||||
if template.Name != "stored_file" {
|
||||
t.Fatalf("resource template name = %q, want %q", template.Name, "stored_file")
|
||||
}
|
||||
if template.URITemplate != "amcs://files/{id}" {
|
||||
t.Fatalf("resource template uri = %q, want %q", template.URITemplate, "amcs://files/{id}")
|
||||
}
|
||||
}
|
||||
|
||||
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
|
||||
t.Helper()
|
||||
|
||||
handler, err := New(config.MCPConfig{
|
||||
ServerName: "test",
|
||||
Version: "0.0.1",
|
||||
SessionTimeout: time.Minute,
|
||||
}, nil, streamableTestToolSet(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("mcpserver.New() error = %v", err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(handler)
|
||||
t.Cleanup(httpServer.Close)
|
||||
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "0.0.1"}, nil)
|
||||
cs, err := client.Connect(context.Background(), &mcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect client: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cs.Close()
|
||||
})
|
||||
|
||||
return cs
|
||||
}
|
||||
Reference in New Issue
Block a user