11package mcp
22
33import (
4+ "bytes"
45 "encoding/json"
56 "errors"
67 "fmt"
78 "reflect"
89 "strconv"
10+
11+ "github.com/santhosh-tekuri/jsonschema"
912)
1013
1114var errToolSchemaConflict = errors .New ("provide either InputSchema or RawInputSchema, not both" )
@@ -36,6 +39,8 @@ type ListToolsResult struct {
3639type CallToolResult struct {
3740 Result
3841 Content []Content `json:"content"` // Can be TextContent, ImageContent, AudioContent, or EmbeddedResource
42+ // Structured content that conforms to the tool's output schema
43+ StructuredContent any `json:"structuredContent,omitempty"`
3944 // Whether the tool call ended in an error.
4045 //
4146 // If not set, this is assumed to be false (the call was successful).
@@ -472,10 +477,17 @@ type ToolListChangedNotification struct {
472477type Tool struct {
473478 // The name of the tool.
474479 Name string `json:"name"`
480+ // A human-friendly display name for the tool.
481+ // This is used for UI display purposes, while Name is used for programmatic identification.
482+ Title string `json:"title,omitempty"`
475483 // A human-readable description of the tool.
476484 Description string `json:"description,omitempty"`
477485 // A JSON Schema object defining the expected parameters for the tool.
478- InputSchema ToolInputSchema `json:"inputSchema"`
486+ InputSchema ToolSchema `json:"inputSchema"`
487+ // A JSON Schema object defining the expected output for the tool.
488+ OutputSchema ToolSchema `json:"outputSchema,omitempty"`
489+ // Compiled JSON schema validator for output validation, cached for performance
490+ compiledOutputSchema * jsonschema.Schema `json:"-"`
479491 // Alternative to InputSchema - allows arbitrary JSON Schema to be provided
480492 RawInputSchema json.RawMessage `json:"-"` // Hide this from JSON marshaling
481493 // Optional properties describing tool behavior
@@ -487,14 +499,83 @@ func (t Tool) GetName() string {
487499 return t .Name
488500}
489501
502+ // GetTitle returns the display title for the tool.
503+ // It follows the precedence: direct title field → annotations.title → empty string.
504+ func (t Tool ) GetTitle () string {
505+ if title := t .Title ; title != "" {
506+ return title
507+ }
508+ return t .Annotations .Title
509+ }
510+
511+ // HasOutputSchema returns true if the tool has an output schema defined.
512+ // This indicates that the tool can return structured content.
513+ func (t Tool ) HasOutputSchema () bool {
514+ return t .OutputSchema .Type != "" && len (t .OutputSchema .Properties ) > 0
515+ }
516+
517+ // validateStructuredOutput performs the actual validation using the compiled schema
518+ func (t Tool ) validateStructuredOutput (result * CallToolResult ) error {
519+ return t .compiledOutputSchema .ValidateInterface (result .StructuredContent )
520+ }
521+
522+ // ensureOutputSchemaValidator compiles and caches the JSON schema validator if not already done
523+ func (t * Tool ) ensureOutputSchemaValidator () error {
524+ if t .compiledOutputSchema != nil {
525+ return nil
526+ }
527+
528+ schemaBytes , err := t .OutputSchema .MarshalJSON ()
529+ if err != nil {
530+ return err
531+ }
532+
533+ compiler := jsonschema .NewCompiler ()
534+
535+ const validatorKey = "output-schema-validator"
536+ if err := compiler .AddResource (validatorKey , bytes .NewReader (schemaBytes )); err != nil {
537+ return err
538+ }
539+
540+ compiledSchema , err := compiler .Compile (validatorKey )
541+ if err != nil {
542+ return err
543+ }
544+
545+ t .compiledOutputSchema = compiledSchema
546+ return nil
547+ }
548+
549+ // ValidateStructuredOutput validates the structured content against the tool's output schema.
550+ // Returns nil if the tool has no output schema or if validation passes.
551+ // Returns an error if the tool has an output schema but the structured content is invalid.
552+ func (t Tool ) ValidateStructuredOutput (result * CallToolResult ) error {
553+ if ! t .HasOutputSchema () {
554+ return nil
555+ }
556+
557+ if result .StructuredContent == nil {
558+ return fmt .Errorf ("tool %s has output schema but structuredContent is nil" , t .Name )
559+ }
560+
561+ if err := t .ensureOutputSchemaValidator (); err != nil {
562+ return err
563+ }
564+
565+ return t .validateStructuredOutput (result )
566+ }
567+
490568// MarshalJSON implements the json.Marshaler interface for Tool.
491569// It handles marshaling either InputSchema or RawInputSchema based on which is set.
492570func (t Tool ) MarshalJSON () ([]byte , error ) {
493571 // Create a map to build the JSON structure
494- m := make (map [string ]any , 3 )
572+ m := make (map [string ]any , 6 )
495573
496- // Add the name and description
574+ // Add the name and title
497575 m ["name" ] = t .Name
576+ if t .Title != "" {
577+ m ["title" ] = t .Title
578+ }
498579 if t .Description != "" {
499580 m ["description" ] = t .Description
500581 }
@@ -510,29 +591,34 @@ func (t Tool) MarshalJSON() ([]byte, error) {
510591 m ["inputSchema" ] = t .InputSchema
511592 }
512593
594+ // Add output schema if defined
595+ if t .HasOutputSchema () {
596+ m ["outputSchema" ] = t .OutputSchema
597+ }
598+
513599 m ["annotations" ] = t .Annotations
514600
515601 return json .Marshal (m )
516602}
517603
518- type ToolInputSchema struct {
604+ type ToolSchema struct {
519605 Type string `json:"type"`
520606 Properties map [string ]any `json:"properties,omitempty"`
521607 Required []string `json:"required,omitempty"`
522608}
523609
524- // MarshalJSON implements the json.Marshaler interface for ToolInputSchema .
525- func (tis ToolInputSchema ) MarshalJSON () ([]byte , error ) {
610+ // MarshalJSON implements the json.Marshaler interface for ToolSchema .
611+ func (schema ToolSchema ) MarshalJSON () ([]byte , error ) {
526612 m := make (map [string ]any )
527- m ["type" ] = tis .Type
613+ m ["type" ] = schema .Type
528614
529615 // Marshal Properties to '{}' rather than `nil` when its length equals zero
530- if tis .Properties != nil {
531- m ["properties" ] = tis .Properties
616+ if schema .Properties != nil {
617+ m ["properties" ] = schema .Properties
532618 }
533619
534- if len (tis .Required ) > 0 {
535- m ["required" ] = tis .Required
620+ if len (schema .Required ) > 0 {
621+ m ["required" ] = schema .Required
536622 }
537623
538624 return json .Marshal (m )
@@ -569,11 +655,17 @@ type PropertyOption func(map[string]any)
569655func NewTool (name string , opts ... ToolOption ) Tool {
570656 tool := Tool {
571657 Name : name ,
572- InputSchema : ToolInputSchema {
658+ InputSchema : ToolSchema {
573659 Type : "object" ,
574660 Properties : make (map [string ]any ),
575661 Required : nil , // Will be omitted from JSON if empty
576662 },
663+ OutputSchema : ToolSchema {
664+ Type : "" ,
665+ Properties : make (map [string ]any ),
666+ Required : nil , // Will be omitted from JSON if empty
667+ },
668+ compiledOutputSchema : nil ,
577669 Annotations : ToolAnnotation {
578670 Title : "" ,
579671 ReadOnlyHint : ToBoolPtr (false ),
@@ -615,6 +707,14 @@ func WithDescription(description string) ToolOption {
615707 }
616708}
617709
710+ // WithTitle sets the direct title field of the Tool.
711+ // This title takes precedence over the annotation title when displaying the tool.
712+ func WithTitle (title string ) ToolOption {
713+ return func (t * Tool ) {
714+ t .Title = title
715+ }
716+ }
717+
618718// WithToolAnnotation adds optional hints about the Tool.
619719func WithToolAnnotation (annotation ToolAnnotation ) ToolOption {
620720 return func (t * Tool ) {
@@ -1076,3 +1176,151 @@ func WithBooleanItems(opts ...PropertyOption) PropertyOption {
10761176 schema ["items" ] = itemSchema
10771177 }
10781178}
1179+
1180+ //
1181+ // Output Schema Configuration Functions
1182+ //
1183+
1184+ // WithOutputSchema sets the output schema for the Tool.
1185+ // This allows the tool to define the structure of its return data.
1186+ func WithOutputSchema (schema ToolSchema ) ToolOption {
1187+ return func (t * Tool ) {
1188+ t .OutputSchema = schema
1189+ }
1190+ }
1191+
1192+ // WithOutputBoolean adds a boolean property to the tool's output schema.
1193+ // It accepts property options to configure the boolean property's behavior and constraints.
1194+ func WithOutputBoolean (name string , opts ... PropertyOption ) ToolOption {
1195+ return func (t * Tool ) {
1196+ // Initialize output schema if not set
1197+ if t .OutputSchema .Type == "" {
1198+ t .OutputSchema .Type = "object"
1199+ }
1200+
1201+ schema := map [string ]any {
1202+ "type" : "boolean" ,
1203+ }
1204+
1205+ for _ , opt := range opts {
1206+ opt (schema )
1207+ }
1208+
1209+ // Remove required from property schema and add to OutputSchema.required
1210+ if required , ok := schema ["required" ].(bool ); ok && required {
1211+ delete (schema , "required" )
1212+ t .OutputSchema .Required = append (t .OutputSchema .Required , name )
1213+ }
1214+
1215+ t .OutputSchema .Properties [name ] = schema
1216+ }
1217+ }
1218+
1219+ // WithOutputNumber adds a number property to the tool's output schema.
1220+ // It accepts property options to configure the number property's behavior and constraints.
1221+ func WithOutputNumber (name string , opts ... PropertyOption ) ToolOption {
1222+ return func (t * Tool ) {
1223+ // Initialize output schema if not set
1224+ if t .OutputSchema .Type == "" {
1225+ t .OutputSchema .Type = "object"
1226+ }
1227+
1228+ schema := map [string ]any {
1229+ "type" : "number" ,
1230+ }
1231+
1232+ for _ , opt := range opts {
1233+ opt (schema )
1234+ }
1235+
1236+ // Remove required from property schema and add to OutputSchema.required
1237+ if required , ok := schema ["required" ].(bool ); ok && required {
1238+ delete (schema , "required" )
1239+ t .OutputSchema .Required = append (t .OutputSchema .Required , name )
1240+ }
1241+
1242+ t .OutputSchema .Properties [name ] = schema
1243+ }
1244+ }
1245+
1246+ // WithOutputString adds a string property to the tool's output schema.
1247+ // It accepts property options to configure the string property's behavior and constraints.
1248+ func WithOutputString (name string , opts ... PropertyOption ) ToolOption {
1249+ return func (t * Tool ) {
1250+ // Initialize output schema if not set
1251+ if t .OutputSchema .Type == "" {
1252+ t .OutputSchema .Type = "object"
1253+ }
1254+
1255+ schema := map [string ]any {
1256+ "type" : "string" ,
1257+ }
1258+
1259+ for _ , opt := range opts {
1260+ opt (schema )
1261+ }
1262+
1263+ // Remove required from property schema and add to OutputSchema.required
1264+ if required , ok := schema ["required" ].(bool ); ok && required {
1265+ delete (schema , "required" )
1266+ t .OutputSchema .Required = append (t .OutputSchema .Required , name )
1267+ }
1268+
1269+ t .OutputSchema .Properties [name ] = schema
1270+ }
1271+ }
1272+
1273+ // WithOutputObject adds an object property to the tool's output schema.
1274+ // It accepts property options to configure the object property's behavior and constraints.
1275+ func WithOutputObject (name string , opts ... PropertyOption ) ToolOption {
1276+ return func (t * Tool ) {
1277+ // Initialize output schema if not set
1278+ if t .OutputSchema .Type == "" {
1279+ t .OutputSchema .Type = "object"
1280+ }
1281+
1282+ schema := map [string ]any {
1283+ "type" : "object" ,
1284+ "properties" : map [string ]any {},
1285+ }
1286+
1287+ for _ , opt := range opts {
1288+ opt (schema )
1289+ }
1290+
1291+ // Remove required from property schema and add to OutputSchema.required
1292+ if required , ok := schema ["required" ].(bool ); ok && required {
1293+ delete (schema , "required" )
1294+ t .OutputSchema .Required = append (t .OutputSchema .Required , name )
1295+ }
1296+
1297+ t .OutputSchema .Properties [name ] = schema
1298+ }
1299+ }
1300+
1301+ // WithOutputArray adds an array property to the tool's output schema.
1302+ // It accepts property options to configure the array property's behavior and constraints.
1303+ func WithOutputArray (name string , opts ... PropertyOption ) ToolOption {
1304+ return func (t * Tool ) {
1305+ // Initialize output schema if not set
1306+ if t .OutputSchema .Type == "" {
1307+ t .OutputSchema .Type = "object"
1308+ }
1309+
1310+ schema := map [string ]any {
1311+ "type" : "array" ,
1312+ }
1313+
1314+ for _ , opt := range opts {
1315+ opt (schema )
1316+ }
1317+
1318+ // Remove required from property schema and add to OutputSchema.required
1319+ if required , ok := schema ["required" ].(bool ); ok && required {
1320+ delete (schema , "required" )
1321+ t .OutputSchema .Required = append (t .OutputSchema .Required , name )
1322+ }
1323+
1324+ t .OutputSchema .Properties [name ] = schema
1325+ }
1326+ }
0 commit comments