diff --git a/README.md b/README.md index 527dfa9..9ee6495 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,17 @@ Here's how to integrate with some popular clients assuming you have a configurat } ``` +## Structured Tool Output + +Every tool declares an `outputSchema` and returns results in two forms on the same response: + +- `content` — a human-readable `TextContent` block containing the output serialized as JSON. Kept for backward compatibility with clients that only render text. +- `structuredContent` — the typed, parseable object. New clients should prefer this for programmatic consumption. + +Schemas are auto-generated from each tool's Go `Output` struct via [`github.com/google/jsonschema-go`](https://pkg.go.dev/github.com/google/jsonschema-go), which emits **JSON Schema draft 2020-12**. The MCP SDK validates every response against the declared schema before sending, so clients can rely on the shape. Field-level descriptions live as `jsonschema:"..."` tags on the `Output` struct in each tool's `pkg/tools//tool.go`. + +To discover the live schema for any tool, inspect the `outputSchema` field returned by a `tools/list` MCP request against a running server. + ## Enabling or disabling specific tools You can enable or disable specific tools by passing command line parameters, setting environment variables, or customizing the `mcp.yaml` configuration file. diff --git a/pkg/chip/output_schema_test.go b/pkg/chip/output_schema_test.go new file mode 100644 index 0000000..df4cd22 --- /dev/null +++ b/pkg/chip/output_schema_test.go @@ -0,0 +1,61 @@ +package chip_test + +import ( + "encoding/json" + "log" + "net/http" + "testing" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/tools" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// Validates all tool output schemas against 2020-12 schema +func TestAllToolsDeclareValid2020_12OutputSchemas(t *testing.T) { + server := chip.NewServer() + tools.RegisterAll(server, &http.Client{}, &chip.ServerToolConfig{}) + + t1, t2 := mcp.NewInMemoryTransports() + if _, err := server.Connect(t.Context(), t1, nil); err != nil { + log.Fatal(err) + } + client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "v0.0.0"}, nil) + session, err := client.Connect(t.Context(), t2, nil) + if err != nil { + log.Fatal(err) + } + defer func() { _ = session.Close() }() + + result, err := session.ListTools(t.Context(), nil) + if err != nil { + t.Fatalf("list tools: %v", err) + } + if len(result.Tools) == 0 { + t.Fatal("no tools registered") + } + + for _, tool := range result.Tools { + t.Run(tool.Name, func(t *testing.T) { + if tool.OutputSchema == nil { + t.Fatalf("tool %q has no outputSchema", tool.Name) + } + + raw, err := json.Marshal(tool.OutputSchema) + if err != nil { + t.Fatalf("marshaling outputSchema: %v", err) + } + var schema jsonschema.Schema + if err := json.Unmarshal(raw, &schema); err != nil { + t.Fatalf("unmarshaling outputSchema: %v", err) + } + + // Resolve validates the schema against the 2020-12 meta-schema. + // A non-nil error means the schema is not valid 2020-12. + if _, err := schema.Resolve(nil); err != nil { + t.Fatalf("outputSchema is not valid JSON Schema 2020-12: %v", err) + } + }) + } +}