Skip to content

Commit b300907

Browse files
authored
Merge branch 'mark3labs:main' into main
2 parents d22932e + f60537b commit b300907

File tree

17 files changed

+2040
-32
lines changed

17 files changed

+2040
-32
lines changed

client/transport/oauth.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type OAuthConfig struct {
3434
AuthServerMetadataURL string
3535
// PKCEEnabled enables PKCE for the OAuth flow (recommended for public clients)
3636
PKCEEnabled bool
37+
// HTTPClient is an optional HTTP client to use for requests.
38+
// If nil, a default HTTP client with a 30 second timeout will be used.
39+
HTTPClient *http.Client
3740
}
3841

3942
// TokenStore is an interface for storing and retrieving OAuth tokens.
@@ -151,10 +154,13 @@ func NewOAuthHandler(config OAuthConfig) *OAuthHandler {
151154
if config.TokenStore == nil {
152155
config.TokenStore = NewMemoryTokenStore()
153156
}
157+
if config.HTTPClient == nil {
158+
config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
159+
}
154160

155161
return &OAuthHandler{
156162
config: config,
157-
httpClient: &http.Client{Timeout: 30 * time.Second},
163+
httpClient: config.HTTPClient,
158164
}
159165
}
160166

examples/typed_tools/main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ type GreetingArgs struct {
1818
Location string `json:"location"`
1919
Timezone string `json:"timezone"`
2020
} `json:"metadata"`
21+
AnyData any `json:"any_data"`
2122
}
2223

24+
// main starts the MCP-based example server, registers a typed "greeting" tool, and serves it over standard I/O.
25+
//
26+
// The registered tool exposes a schema for typed inputs (name, age, is_vip, languages, metadata, and any_data)
27+
// and uses a typed handler to produce personalized greetings. If the server fails to start, an error is printed to stdout.
2328
func main() {
2429
// Create a new MCP server
2530
s := server.NewMCPServer(
@@ -61,6 +66,9 @@ func main() {
6166
},
6267
}),
6368
),
69+
mcp.WithAny("any_data",
70+
mcp.Description("Any kind of data, e.g., an integer"),
71+
),
6472
)
6573

6674
// Add tool handler using the typed handler
@@ -72,7 +80,11 @@ func main() {
7280
}
7381
}
7482

75-
// Our typed handler function that receives strongly-typed arguments
83+
// typedGreetingHandler constructs a personalized greeting from the provided GreetingArgs and returns it as a text tool result.
84+
//
85+
// If args.Name is empty the function returns a tool error result with the message "name is required" and a nil error.
86+
// The returned greeting may include the caller's age, a VIP acknowledgement, the number and list of spoken languages,
87+
// location and timezone from metadata, and a formatted representation of AnyData when present.
7688
func typedGreetingHandler(ctx context.Context, request mcp.CallToolRequest, args GreetingArgs) (*mcp.CallToolResult, error) {
7789
if args.Name == "" {
7890
return mcp.NewToolResultError("name is required"), nil
@@ -101,5 +113,9 @@ func typedGreetingHandler(ctx context.Context, request mcp.CallToolRequest, args
101113
}
102114
}
103115

116+
if args.AnyData != nil {
117+
greeting += fmt.Sprintf(" I also received some other data: %v.", args.AnyData)
118+
}
119+
104120
return mcp.NewToolResultText(greeting), nil
105121
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/mark3labs/mcp-go
22

3-
go 1.23
3+
go 1.23.0
44

55
require (
66
github.com/google/uuid v1.6.0

mcp/tools.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,8 +1091,10 @@ func WithObject(name string, opts ...PropertyOption) ToolOption {
10911091
}
10921092
}
10931093

1094-
// WithArray adds an array property to the tool schema.
1095-
// It accepts property options to configure the array property's behavior and constraints.
1094+
// WithArray returns a ToolOption that adds an array-typed property with the given name to a Tool's input schema.
1095+
// It applies provided PropertyOption functions to configure the property's schema, moves a `required` flag
1096+
// from the property schema into the Tool's InputSchema.Required slice when present, and registers the resulting
1097+
// schema under InputSchema.Properties[name].
10961098
func WithArray(name string, opts ...PropertyOption) ToolOption {
10971099
return func(t *Tool) {
10981100
schema := map[string]any{
@@ -1113,7 +1115,29 @@ func WithArray(name string, opts ...PropertyOption) ToolOption {
11131115
}
11141116
}
11151117

1116-
// Properties defines the properties for an object schema
1118+
// WithAny adds an input property named name with no predefined JSON Schema type to the Tool's input schema.
1119+
// The returned ToolOption applies the provided PropertyOption functions to the property's schema, moves a property-level
1120+
// `required` flag into the Tool's InputSchema.Required list if present, and stores the resulting schema under InputSchema.Properties[name].
1121+
func WithAny(name string, opts ...PropertyOption) ToolOption {
1122+
return func(t *Tool) {
1123+
schema := map[string]any{}
1124+
1125+
for _, opt := range opts {
1126+
opt(schema)
1127+
}
1128+
1129+
// Remove required from property schema and add to InputSchema.required
1130+
if required, ok := schema["required"].(bool); ok && required {
1131+
delete(schema, "required")
1132+
t.InputSchema.Required = append(t.InputSchema.Required, name)
1133+
}
1134+
1135+
t.InputSchema.Properties[name] = schema
1136+
}
1137+
}
1138+
1139+
// Properties sets the "properties" map for an object schema.
1140+
// The returned PropertyOption stores the provided map under the schema's "properties" key.
11171141
func Properties(props map[string]any) PropertyOption {
11181142
return func(schema map[string]any) {
11191143
schema["properties"] = props

mcp/tools_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,108 @@ func TestToolWithObjectAndArray(t *testing.T) {
242242
assert.Contains(t, required, "books")
243243
}
244244

245+
func TestToolWithAny(t *testing.T) {
246+
const desc = "Can be any value: string, number, bool, object, or slice"
247+
248+
tool := NewTool("any-tool",
249+
WithDescription("A tool with an 'any' type property"),
250+
WithAny("data",
251+
Description(desc),
252+
Required(),
253+
),
254+
)
255+
256+
data, err := json.Marshal(tool)
257+
assert.NoError(t, err)
258+
259+
var result map[string]any
260+
err = json.Unmarshal(data, &result)
261+
assert.NoError(t, err)
262+
263+
assert.Equal(t, "any-tool", result["name"])
264+
265+
schema, ok := result["inputSchema"].(map[string]any)
266+
assert.True(t, ok)
267+
assert.Equal(t, "object", schema["type"])
268+
269+
properties, ok := schema["properties"].(map[string]any)
270+
assert.True(t, ok)
271+
272+
dataProp, ok := properties["data"].(map[string]any)
273+
assert.True(t, ok)
274+
_, typeExists := dataProp["type"]
275+
assert.False(t, typeExists, "The 'any' type property should not have a 'type' field")
276+
assert.Equal(t, desc, dataProp["description"])
277+
278+
required, ok := schema["required"].([]any)
279+
assert.True(t, ok)
280+
assert.Contains(t, required, "data")
281+
282+
type testStruct struct {
283+
A string `json:"A"`
284+
}
285+
testCases := []struct {
286+
description string
287+
arg any
288+
expect any
289+
}{{
290+
description: "string",
291+
arg: "hello world",
292+
expect: "hello world",
293+
}, {
294+
description: "integer",
295+
arg: 123,
296+
expect: float64(123), // JSON unmarshals numbers to float64
297+
}, {
298+
description: "float",
299+
arg: 3.14,
300+
expect: 3.14,
301+
}, {
302+
description: "boolean",
303+
arg: true,
304+
expect: true,
305+
}, {
306+
description: "object",
307+
arg: map[string]any{"key": "value"},
308+
expect: map[string]any{"key": "value"},
309+
}, {
310+
description: "slice",
311+
arg: []any{1, "two", false},
312+
expect: []any{float64(1), "two", false},
313+
}, {
314+
description: "struct",
315+
arg: testStruct{A: "B"},
316+
expect: map[string]any{"A": "B"},
317+
}}
318+
319+
for _, tc := range testCases {
320+
t.Run(fmt.Sprintf("with_%s", tc.description), func(t *testing.T) {
321+
req := CallToolRequest{
322+
Request: Request{},
323+
Params: CallToolParams{
324+
Name: "any-tool",
325+
Arguments: map[string]any{
326+
"data": tc.arg,
327+
},
328+
},
329+
}
330+
331+
// Marshal and unmarshal to simulate a real request
332+
reqBytes, err := json.Marshal(req)
333+
assert.NoError(t, err)
334+
335+
var unmarshaledReq CallToolRequest
336+
err = json.Unmarshal(reqBytes, &unmarshaledReq)
337+
assert.NoError(t, err)
338+
339+
args := unmarshaledReq.GetArguments()
340+
value, ok := args["data"]
341+
assert.True(t, ok)
342+
assert.Equal(t, tc.expect, value)
343+
})
344+
}
345+
}
346+
245347
func TestParseToolCallToolRequest(t *testing.T) {
246348
request := CallToolRequest{}
247349
request.Params.Name = "test-tool"

server/errors.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ var (
1313
ErrToolNotFound = errors.New("tool not found")
1414

1515
// Session-related errors
16-
ErrSessionNotFound = errors.New("session not found")
17-
ErrSessionExists = errors.New("session already exists")
18-
ErrSessionNotInitialized = errors.New("session not properly initialized")
19-
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
20-
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
16+
ErrSessionNotFound = errors.New("session not found")
17+
ErrSessionExists = errors.New("session already exists")
18+
ErrSessionNotInitialized = errors.New("session not properly initialized")
19+
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
20+
ErrSessionDoesNotSupportResources = errors.New("session does not support per-session resources")
21+
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
2122

2223
// Notification-related errors
2324
ErrNotificationNotInitialized = errors.New("notification channel not initialized")

server/server.go

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
package server
33

44
import (
5+
"cmp"
56
"context"
67
"encoding/base64"
78
"encoding/json"
89
"fmt"
10+
"maps"
911
"slices"
1012
"sort"
1113
"sync"
@@ -838,21 +840,36 @@ func (s *MCPServer) handleListResources(
838840
request mcp.ListResourcesRequest,
839841
) (*mcp.ListResourcesResult, *requestError) {
840842
s.resourcesMu.RLock()
841-
resources := make([]mcp.Resource, 0, len(s.resources))
842-
for _, entry := range s.resources {
843-
resources = append(resources, entry.resource)
843+
resourceMap := make(map[string]mcp.Resource, len(s.resources))
844+
for uri, entry := range s.resources {
845+
resourceMap[uri] = entry.resource
844846
}
845847
s.resourcesMu.RUnlock()
846848

849+
// Check if there are session-specific resources
850+
session := ClientSessionFromContext(ctx)
851+
if session != nil {
852+
if sessionWithResources, ok := session.(SessionWithResources); ok {
853+
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
854+
// Merge session-specific resources with global resources
855+
for uri, serverResource := range sessionResources {
856+
resourceMap[uri] = serverResource.Resource
857+
}
858+
}
859+
}
860+
}
861+
847862
// Sort the resources by name
848-
sort.Slice(resources, func(i, j int) bool {
849-
return resources[i].Name < resources[j].Name
863+
resourcesList := slices.SortedFunc(maps.Values(resourceMap), func(a, b mcp.Resource) int {
864+
return cmp.Compare(a.Name, b.Name)
850865
})
866+
867+
// Apply pagination
851868
resourcesToReturn, nextCursor, err := listByPagination(
852869
ctx,
853870
s,
854871
request.Params.Cursor,
855-
resources,
872+
resourcesList,
856873
)
857874
if err != nil {
858875
return nil, &requestError{
@@ -912,9 +929,35 @@ func (s *MCPServer) handleReadResource(
912929
request mcp.ReadResourceRequest,
913930
) (*mcp.ReadResourceResult, *requestError) {
914931
s.resourcesMu.RLock()
932+
933+
// First check session-specific resources
934+
var handler ResourceHandlerFunc
935+
var ok bool
936+
937+
session := ClientSessionFromContext(ctx)
938+
if session != nil {
939+
if sessionWithResources, typeAssertOk := session.(SessionWithResources); typeAssertOk {
940+
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
941+
resource, sessionOk := sessionResources[request.Params.URI]
942+
if sessionOk {
943+
handler = resource.Handler
944+
ok = true
945+
}
946+
}
947+
}
948+
}
949+
950+
// If not found in session tools, check global tools
951+
if !ok {
952+
globalResource, rok := s.resources[request.Params.URI]
953+
if rok {
954+
handler = globalResource.handler
955+
ok = true
956+
}
957+
}
958+
915959
// First try direct resource handlers
916-
if entry, ok := s.resources[request.Params.URI]; ok {
917-
handler := entry.handler
960+
if ok {
918961
s.resourcesMu.RUnlock()
919962

920963
finalHandler := handler

server/server_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,8 @@ func TestMCPServer_HandleValidMessages(t *testing.T) {
445445
resp, ok := response.(mcp.JSONRPCResponse)
446446
assert.True(t, ok)
447447

448-
listResult, ok := resp.Result.(mcp.ListResourcesResult)
448+
_, ok = resp.Result.(mcp.ListResourcesResult)
449449
assert.True(t, ok)
450-
assert.NotNil(t, listResult.Resources)
451450
},
452451
},
453452
}

0 commit comments

Comments
 (0)