Skip to content

Commit 62f00b7

Browse files
committed
Merge branch 'main' of github.com:mark3labs/mcp-go
2 parents a442928 + b1c82f0 commit 62f00b7

13 files changed

Lines changed: 1487 additions & 27 deletions

File tree

examples/everything/main.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"flag"
77
"fmt"
88
"log"
9+
"os"
910
"strconv"
1011
"strings"
1112
"time"
@@ -34,31 +35,33 @@ const (
3435
func NewMCPServer() *server.MCPServer {
3536
hooks := &server.Hooks{}
3637

38+
// All hook output goes to stderr. In stdio mode, stdout is the JSON-RPC
39+
// transport; writing to stdout from hooks corrupts the protocol stream.
3740
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
38-
fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
41+
fmt.Fprintf(os.Stderr, "beforeAny: %s, %v, %v\n", method, id, message)
3942
})
4043
hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
41-
fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
44+
fmt.Fprintf(os.Stderr, "onSuccess: %s, %v, %v, %v\n", method, id, message, result)
4245
})
4346
hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
44-
fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
47+
fmt.Fprintf(os.Stderr, "onError: %s, %v, %v, %v\n", method, id, message, err)
4548
})
4649
hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
47-
fmt.Printf("beforeInitialize: %v, %v\n", id, message)
50+
fmt.Fprintf(os.Stderr, "beforeInitialize: %v, %v\n", id, message)
4851
})
4952
hooks.AddOnRequestInitialization(func(ctx context.Context, id any, message any) error {
50-
fmt.Printf("AddOnRequestInitialization: %v, %v\n", id, message)
53+
fmt.Fprintf(os.Stderr, "AddOnRequestInitialization: %v, %v\n", id, message)
5154
// authorization verification and other preprocessing tasks are performed.
5255
return nil
5356
})
5457
hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
55-
fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
58+
fmt.Fprintf(os.Stderr, "afterInitialize: %v, %v, %v\n", id, message, result)
5659
})
5760
hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result any) {
58-
fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
61+
fmt.Fprintf(os.Stderr, "afterCallTool: %v, %v, %v\n", id, message, result)
5962
})
6063
hooks.AddBeforeCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest) {
61-
fmt.Printf("beforeCallTool: %v, %v\n", id, message)
64+
fmt.Fprintf(os.Stderr, "beforeCallTool: %v, %v\n", id, message)
6265
})
6366

6467
mcpServer := server.NewMCPServer(

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.5
55
require (
66
github.com/google/jsonschema-go v0.4.2
77
github.com/google/uuid v1.6.0
8+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
89
github.com/spf13/cast v1.7.1
910
github.com/stretchr/testify v1.9.0
1011
github.com/yosida95/uritemplate/v3 v3.0.2
@@ -13,5 +14,6 @@ require (
1314
require (
1415
github.com/davecgh/go-spew v1.1.1 // indirect
1516
github.com/pmezard/go-difflib v1.0.0 // indirect
17+
golang.org/x/text v0.14.0 // indirect
1618
gopkg.in/yaml.v3 v3.0.1 // indirect
1719
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
4+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
35
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
46
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
57
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -16,12 +18,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
1618
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1719
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
1820
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
21+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
22+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
1923
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
2024
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
2125
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2226
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2327
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
2428
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
29+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
30+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
2531
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2632
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2733
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

mcp/error_unmarshal_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package mcp
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestJSONRPCErrorDetails_UnmarshalJSON verifies that JSONRPCErrorDetails handles
12+
// both spec-compliant object errors and non-compliant string errors from servers like Slack.
13+
func TestJSONRPCErrorDetails_UnmarshalJSON(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
input string
17+
wantCode int
18+
wantMessage string
19+
wantErr bool
20+
}{
21+
{
22+
name: "standard object",
23+
input: `{"code": -32600, "message": "invalid request"}`,
24+
wantCode: -32600,
25+
wantMessage: "invalid request",
26+
},
27+
{
28+
name: "string error from non-compliant server",
29+
input: `"cursor_invalid"`,
30+
wantCode: INTERNAL_ERROR,
31+
wantMessage: "cursor_invalid",
32+
},
33+
{
34+
name: "object with data field",
35+
input: `{"code": -32603, "message": "something failed", "data": {"detail": "more info"}}`,
36+
wantCode: -32603,
37+
wantMessage: "something failed",
38+
},
39+
{
40+
name: "number is rejected",
41+
input: `42`,
42+
wantErr: true,
43+
},
44+
{
45+
name: "array is rejected",
46+
input: `["bad"]`,
47+
wantErr: true,
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
var details JSONRPCErrorDetails
54+
err := json.Unmarshal([]byte(tt.input), &details)
55+
if tt.wantErr {
56+
require.Error(t, err)
57+
return
58+
}
59+
require.NoError(t, err)
60+
assert.Equal(t, tt.wantCode, details.Code)
61+
assert.Equal(t, tt.wantMessage, details.Message)
62+
})
63+
}
64+
}
65+
66+
// TestJSONRPCResponse_StringError verifies that a full JSON-RPC response with a
67+
// string error field unmarshals correctly when using JSONRPCErrorDetails.
68+
func TestJSONRPCResponse_StringError(t *testing.T) {
69+
raw := `{"jsonrpc":"2.0","id":1,"error":"cursor_invalid"}`
70+
71+
type response struct {
72+
JSONRPC string `json:"jsonrpc"`
73+
ID int `json:"id"`
74+
Error *JSONRPCErrorDetails `json:"error"`
75+
}
76+
77+
var resp response
78+
require.NoError(t, json.Unmarshal([]byte(raw), &resp))
79+
require.NotNil(t, resp.Error)
80+
assert.Equal(t, INTERNAL_ERROR, resp.Error.Code)
81+
assert.Equal(t, "cursor_invalid", resp.Error.Message)
82+
}

mcp/tools.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,33 +1056,33 @@ func Pattern(pattern string) PropertyOption {
10561056
// Number Property Options
10571057
//
10581058

1059-
// DefaultNumber sets the default value for a number property.
1059+
// DefaultNumber sets the default value for a number or integer property.
10601060
// This value will be used if the property is not explicitly provided.
1061-
func DefaultNumber(value float64) PropertyOption {
1061+
func DefaultNumber[T int | int64 | float64](value T) PropertyOption {
10621062
return func(schema map[string]any) {
10631063
schema["default"] = value
10641064
}
10651065
}
10661066

1067-
// Max sets the maximum value for a number property.
1067+
// Max sets the maximum value for a number or integer property.
10681068
// The number value must not exceed this maximum.
1069-
func Max(max float64) PropertyOption {
1069+
func Max[T int | int64 | float64](max T) PropertyOption {
10701070
return func(schema map[string]any) {
10711071
schema["maximum"] = max
10721072
}
10731073
}
10741074

1075-
// Min sets the minimum value for a number property.
1075+
// Min sets the minimum value for a number or integer property.
10761076
// The number value must not be less than this minimum.
1077-
func Min(min float64) PropertyOption {
1077+
func Min[T int | int64 | float64](min T) PropertyOption {
10781078
return func(schema map[string]any) {
10791079
schema["minimum"] = min
10801080
}
10811081
}
10821082

1083-
// MultipleOf specifies that a number must be a multiple of the given value.
1083+
// MultipleOf specifies that a number or integer must be a multiple of the given value.
10841084
// The number value must be divisible by this value.
1085-
func MultipleOf(value float64) PropertyOption {
1085+
func MultipleOf[T int | int64 | float64](value T) PropertyOption {
10861086
return func(schema map[string]any) {
10871087
schema["multipleOf"] = value
10881088
}
@@ -1116,6 +1116,28 @@ func DefaultArray[T any](value []T) PropertyOption {
11161116
// Property Type Helpers
11171117
//
11181118

1119+
// WithInteger adds an integer property to the tool schema.
1120+
// It accepts property options to configure the integer property's behavior and constraints.
1121+
func WithInteger(name string, opts ...PropertyOption) ToolOption {
1122+
return func(t *Tool) {
1123+
schema := map[string]any{
1124+
"type": "integer",
1125+
}
1126+
1127+
for _, opt := range opts {
1128+
opt(schema)
1129+
}
1130+
1131+
// Remove required from property schema and add to InputSchema.required
1132+
if required, ok := schema["required"].(bool); ok && required {
1133+
delete(schema, "required")
1134+
t.InputSchema.Required = append(t.InputSchema.Required, name)
1135+
}
1136+
1137+
t.InputSchema.Properties[name] = schema
1138+
}
1139+
}
1140+
11191141
// WithBoolean adds a boolean property to the tool schema.
11201142
// It accepts property options to configure the boolean property's behavior and constraints.
11211143
func WithBoolean(name string, opts ...PropertyOption) ToolOption {
@@ -1394,6 +1416,35 @@ func WithNumberItems(opts ...PropertyOption) PropertyOption {
13941416
}
13951417
}
13961418

1419+
// WithIntegerItems configures an array's items to be of type integer.
1420+
//
1421+
// Supported options: Description(), DefaultNumber(), Min(), Max(), MultipleOf()
1422+
// Note: Options like Required() are not valid for item schemas and will be ignored.
1423+
//
1424+
// Examples:
1425+
//
1426+
// mcp.WithArray("ids", mcp.WithIntegerItems())
1427+
// mcp.WithArray("scores", mcp.WithIntegerItems(mcp.Min(0), mcp.Max(100)))
1428+
//
1429+
// Limitations: Only supports simple integer arrays. Use Items() for complex objects.
1430+
func WithIntegerItems(opts ...PropertyOption) PropertyOption {
1431+
return func(schema map[string]any) {
1432+
itemSchema := map[string]any{
1433+
"type": "integer",
1434+
}
1435+
1436+
for _, opt := range opts {
1437+
opt(itemSchema)
1438+
}
1439+
1440+
if required, ok := itemSchema["required"].(bool); ok && required {
1441+
delete(itemSchema, "required")
1442+
}
1443+
1444+
schema["items"] = itemSchema
1445+
}
1446+
}
1447+
13971448
// WithBooleanItems configures an array's items to be of type boolean.
13981449
//
13991450
// Supported options: Description(), DefaultBool()

mcp/tools_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,58 @@ func TestUnmarshalToolWithoutRawSchema(t *testing.T) {
145145
assert.Empty(t, toolUnmarshalled.RawInputSchema)
146146
}
147147

148+
func TestToolWithStringInteger(t *testing.T) {
149+
tool := NewTool("buy-item",
150+
WithDescription("A tool for buying items"),
151+
WithString("itemName",
152+
Description("Name of the item to purchase"),
153+
Required(),
154+
),
155+
WithInteger("itemCount",
156+
Description("Number of copies of the item to purchase"),
157+
Required(),
158+
DefaultNumber(1),
159+
Min(1),
160+
Max(100),
161+
),
162+
)
163+
164+
data, err := json.Marshal(tool)
165+
require.NoError(t, err)
166+
167+
var result map[string]any
168+
err = json.Unmarshal(data, &result)
169+
require.NoError(t, err)
170+
171+
assert.Equal(t, "buy-item", result["name"])
172+
assert.Equal(t, "A tool for buying items", result["description"])
173+
174+
schema, ok := result["inputSchema"].(map[string]any)
175+
require.True(t, ok)
176+
assert.Equal(t, "object", schema["type"])
177+
178+
properties, ok := schema["properties"].(map[string]any)
179+
require.True(t, ok)
180+
181+
itemName, ok := properties["itemName"].(map[string]any)
182+
require.True(t, ok)
183+
assert.Equal(t, "string", itemName["type"])
184+
assert.Equal(t, "Name of the item to purchase", itemName["description"])
185+
186+
itemCount, ok := properties["itemCount"].(map[string]any)
187+
require.True(t, ok)
188+
assert.Equal(t, "integer", itemCount["type"])
189+
assert.Equal(t, "Number of copies of the item to purchase", itemCount["description"])
190+
assert.Equal(t, float64(1), itemCount["default"])
191+
assert.Equal(t, float64(1), itemCount["minimum"])
192+
assert.Equal(t, float64(100), itemCount["maximum"])
193+
194+
required, ok := schema["required"].([]any)
195+
require.True(t, ok)
196+
assert.Contains(t, required, "itemName")
197+
assert.Contains(t, required, "itemCount")
198+
}
199+
148200
func TestToolWithObjectAndArray(t *testing.T) {
149201
// Create a tool with both object and array properties
150202
tool := NewTool("reading-list",
@@ -1486,6 +1538,46 @@ func TestNewItemsAPICompatibility(t *testing.T) {
14861538
),
14871539
),
14881540
},
1541+
{
1542+
name: "WithIntegerItems basic",
1543+
oldTool: NewTool("old-integer-basic",
1544+
WithDescription("Tool with integer array using old API"),
1545+
WithArray("scores",
1546+
Description("List of scores"),
1547+
Items(map[string]any{
1548+
"type": "integer",
1549+
}),
1550+
),
1551+
),
1552+
newTool: NewTool("new-integer-basic",
1553+
WithDescription("Tool with integer array using new API"),
1554+
WithArray("scores",
1555+
Description("List of scores"),
1556+
WithIntegerItems(),
1557+
),
1558+
),
1559+
},
1560+
{
1561+
name: "WithIntegerItems with constraints",
1562+
oldTool: NewTool("old-integer-with-constraints",
1563+
WithDescription("Tool with constrained integer array using old API"),
1564+
WithArray("ratings",
1565+
Description("List of ratings"),
1566+
Items(map[string]any{
1567+
"type": "integer",
1568+
"minimum": 0,
1569+
"maximum": 10,
1570+
}),
1571+
),
1572+
),
1573+
newTool: NewTool("new-integer-with-constraints",
1574+
WithDescription("Tool with constrained integer array using new API"),
1575+
WithArray("ratings",
1576+
Description("List of ratings"),
1577+
WithIntegerItems(Min(0), Max(10)),
1578+
),
1579+
),
1580+
},
14891581
{
14901582
name: "WithBooleanItems basic",
14911583
oldTool: NewTool("old-boolean-array",

0 commit comments

Comments
 (0)