internal/mcp/design: further clean-up

Do a pass through the design doc, adding a bit more clean-up and
commentary.

Additionally, update ServerTool and ServerPrompt to be consistent with
the design.

Change-Id: I1b75c4690d86fe8946d512cb36a1fc2938b3c883
Reviewed-on: https://go-review.googlesource.com/c/tools/+/672576
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Sam Thanawalla <samthanawalla@google.com>
Auto-Submit: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Rob Findley 2025-05-14 18:28:24 +00:00 • committed by Gopher Robot
commit 7eadde0a23
8 changed files with 168 additions and 121 deletions

View file

@ -4,10 +4,12 @@ This document discusses the design of a Go SDK for the [model context
protocol](https://modelcontextprotocol.io/specification/2025-03-26). It is
intended to seed a GitHub discussion about the official Go MCP SDK.
The github.com/modelcontextprotocol/go-sdk/mcp package contains a prototype that we built
to explore the MCP design space. Many of the ideas there are present in this
document. However, we have diverged and expanded on the APIs of that prototype,
and this document should be considered canonical.
The
[github.com/modelcontextprotocol/go-sdk/mcp](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp@master)
package contains a prototype that we built to explore the MCP design space.
Many of the ideas there are present in this document. However, we have diverged
from and expanded on the APIs of that prototype, and this document should be
considered canonical.
## Similarities and differences with mark3labs/mcp-go
@ -24,9 +26,7 @@ not viable for an official SDK. In yet others, we simply think there is room for
API refinement, and we should take this opportunity to consider our options.
Therefore, we wrote this document as though it were proposing a new
implementation. Nevertheless, much of the API discussed here originated from or
was inspired by mcp-go and other unofficial SDKs, and if the consensus of this
discussion is close enough to mcp-go or any other unofficial SDK, we can start
from a fork.
was inspired by mcp-go and other unofficial SDKs.
Since mcp-go is so influential and popular, we have noted significant
differences from its API in the sections below. Although the API here is not
@ -58,8 +58,7 @@ SDK. An official SDK should aim to be:
In the sections below, we visit each aspect of the MCP spec, in approximately
the order they are presented by the [official spec](https://modelcontextprotocol.io/specification/2025-03-26)
For each, we discuss considerations for the Go implementation. In many cases an
API is suggested, though in some there may be open questions.
For each, we discuss considerations for the Go implementation, and propose a Go API.
## Foundations
@ -68,22 +67,26 @@ API is suggested, though in some there may be open questions.
In the sections that follow, it is assumed that most of the MCP API lives in a
single shared package, the `mcp` package. This is inconsistent with other MCP
SDKs, but is consistent with Go packages like `net/http`, `net/rpc`, or
`google.golang.org/grpc`.
`google.golang.org/grpc`. We believe that having a single package aids
discoverability in package documentation and in the IDE. Furthermore, it avoids
somwhat arbitrary decisions about package structure that may be rendered
inaccurate by future evolution of the spec.
Functionality that is not directly related to MCP (like jsonschema or jsonrpc2)
belongs in a separate package.
Therefore, this is the package layout. `module.path` is a placeholder for the
final module path of the mcp module
Therefore, this is the package layout, assuming
github.com/modelcontextprotocol/go-sdk as the module path.
- `module.path/mcp`: the bulk of the user facing API
- `module.path/jsonschema`: a jsonschema implementation, with validation
- `module.path/internal/jsonrpc2`: a fork of x/tools/internal/jsonrpc2_v2
- `github.com/modelcontextprotocol/go-sdk/mcp`: the bulk of the user facing API
- `github.com/modelcontextprotocol/go-sdk/jsonschema`: a jsonschema implementation, with validation
- `github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2`: a fork of x/tools/internal/jsonrpc2_v2
The JSON-RPC implementation is hidden, to avoid tight coupling. As described in
the next section, the only aspects of JSON-RPC that need to be exposed in the
SDK are the message types, for the purposes of defining custom transports. We
can expose these types from the `mcp` package via aliases or wrappers.
can expose these types by promoting them from the `mcp` package using aliases
or wrappers.
**Difference from mcp-go**: Our `mcp` package includes all the functionality of
mcp-go's `mcp`, `client`, `server` and `transport` packages.
@ -98,12 +101,15 @@ defines two transports:
- **streamable http**: communication over a relatively complicated series of
text/event-stream GET and HTTP POST requests.
Additionally, version `2024-11-05` of the spec defined a simpler HTTP transport:
Additionally, version `2024-11-05` of the spec defined a simpler (yet stateful)
HTTP transport:
- **sse**: client issues a hanging GET request and receives messages via
`text/event-stream`, and sends messages via POST to a session endpoint.
Furthermore, the spec [states](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#custom-transports) that it must be possible for users to define their own custom transports.
Furthermore, the spec
[states](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#custom-transports)
that it must be possible for users to define their own custom transports.
Given the diversity of the transport implementations, they can be challenging
to abstract. However, since JSON-RPC requires a bidirectional stream, we can
@ -124,13 +130,12 @@ type Stream interface {
}
```
Methods accept a Go `Context` and return an `error`,
as is idiomatic for APIs that do I/O.
Methods accept a Go `Context` and return an `error`, as is idiomatic for APIs
that do I/O.
A `Transport` is something that connects a logical JSON-RPC
stream, and nothing more. Streams must be closeable in order to
implement client and server shutdown, and therefore conform to the `io.Closer`
interface.
A `Transport` is something that connects a logical JSON-RPC stream, and nothing
more. Streams must be closeable in order to implement client and server
shutdown, and therefore conform to the `io.Closer` interface.
Other SDKs define higher-level transports, with, for example, methods to send a
notification or make a call. Those are jsonrpc2 operations on top of the
@ -142,6 +147,16 @@ language server `gopls`, which we propose to fork for the MCP SDK. It already
handles concerns like client/server connection, request lifecycle,
cancellation, and shutdown.
**Differences from mcp-go**: The Go team has a battle-tested JSON-RPC
implementation that we use for gopls, our Go LSP server. We are using the new
version of this library as part of our MCP SDK. It handles all JSON-RPC 2.0
features, including cancellation.
The `Transport` interface here is lower-level than that of mcp-go, but serves a
similar purpose. We believe the lower-level interface is easier to implement.
#### stdio transports
In the MCP Spec, the **stdio** transport uses newline-delimited JSON to
communicate over stdin/stdout. It's possible to model both client side and
server side of this communication with a shared type that communicates over an
@ -178,13 +193,20 @@ func NewStdIOTransport() *StdIOTransport
func (t *StdIOTransport) Connect(context.Context) (Stream, error)
```
#### HTTP transports
The HTTP transport APIs are even more asymmetrical. Since connections are initiated
via HTTP requests, the client developer will create a transport, but
the server developer will typically install an HTTP handler. Internally, the
HTTP handler will create a transport for each new client connection.
HTTP handler will create a logical transport for each new client connection.
Importantly, since they serve many connections, the HTTP handlers must accept a
callback to get an MCP server for each new session.
callback to get an MCP server for each new session. As described below, MCP
servers can optionally connect to multiple clients. This allows customization
of per-session servers: if the MCP server is stateless, the user can return the
same MCP server for each connection. On the other hand, if any per-session
customization is required, it is possible by returning a different `Server`
instance for each connection.
```go
// SSEHTTPHandler is an http.Handler that serves SSE-based MCP sessions as defined by
@ -212,7 +234,7 @@ see [Middleware](#Middleware) below.
By default, the SSE handler creates messages endpoints with the
`?sessionId=...` query parameter. Users that want more control over the
management of sessions and session endpoints may write their own handler, and
create `SSEServerTransport` instances themselves, for incoming GET requests.
create `SSEServerTransport` instances themselves for incoming GET requests.
```go
// A SSEServerTransport is a logical SSE session created through a hanging GET
@ -233,6 +255,9 @@ type SSEServerTransport struct { /* ... */ }
// The transport is itself an [http.Handler]. It is the caller's responsibility
// to ensure that the resulting transport serves HTTP requests on the given
// session endpoint.
//
// Most callers should instead use an [SSEHandler], which transparently handles
// the delegation to SSEServerTransports.
func NewSSEServerTransport(endpoint string, w http.ResponseWriter) *SSEServerTransport
// ServeHTTP handles POST requests to the transport endpoint.
@ -282,7 +307,27 @@ func NewStreamableClientTransport(url string) *StreamableClientTransport {
func (*StreamableClientTransport) Connect(context.Context) (Stream, error)
```
Finally, we also provide a couple of transport implementations for special scenarios.
**Differences from mcp-go**: In mcp-go, server authors create an `MCPServer`,
populate it with tools, resources and so on, and then wrap it in an `SSEServer`
or `StdioServer`. Users can manage their own sessions with `RegisterSession`
and `UnregisterSession`. Rather than use a server constructor to get a distinct
server for each connection, there is a concept of a "session tool" that
overlays tools for a specific session.
We find the similarity in names among the three server types to be confusing,
and we could not discover any uses of the session methods in the open-source
ecosystem. Furthermore, we believe that a server factor (`getServer`) provides
equivalent functionality as the per-session logic of mcp-go, with a smaller API
surface and fewer overlapping concepts.
Additionally, individual handlers and transports here have a minimal API, and
do not expose internal details. Customization of things like handlers or
session management is intended to be implemented with middleware and/or
compositional patterns.
#### Other transports
We also provide a couple of transport implementations for special scenarios.
An InMemoryTransport can be used when the client and server reside in the same
process. A LoggingTransport is a middleware layer that logs RPC logs to a desired
location, specified as an io.Writer.
@ -302,32 +347,6 @@ type LoggingTransport struct { /* ... */ }
func NewLoggingTransport(delegate Transport, w io.Writer) *LoggingTransport
```
**Differences from mcp-go**: The Go team has a battle-tested JSON-RPC
implementation that we use for gopls, our Go LSP server. We are using the new
version of this library as part of our MCP SDK. It handles all JSON-RPC 2.0
features, including cancellation.
The `Transport` interface here is lower-level than that of mcp-go, but serves a
similar purpose. We believe the lower-level interface is easier to implement.
In mcp-go, server authors create an `MCPServer`, populate it with tools,
resources and so on, and then wrap it in an `SSEServer` or `StdioServer`. These
also use session IDs, which are exposed. Users can manage their own sessions
with `RegisterSession` and `UnregisterSession`.
We find the similarity in names among the three server types to be confusing,
and we could not discover any uses of the session methods in the open-source
ecosystem. In our design, server authors create a `Server`, and then
connect it to a `Transport`. An `SSEHTTPHandler` manages sessions for
incoming SSE connections, but does not expose them. HTTP handlers accept a
server constructor, rather than Server, to allow for stateful or "per-session"
servers.
Individual handlers and transports here have a minimal smaller API, and do not
expose internal details. Customization of things like handlers or session
management is intended to be implemented with middleware and/or compositional
patterns.
### Protocol types
Types needed for the protocol are generated from the
@ -373,7 +392,7 @@ type Content struct {
```
**Differences from mcp-go**: these types are largely similar, but our type
generation flattens types rather than using struct embedding.
generator flattens types rather than using struct embedding.
### Clients and Servers
@ -387,13 +406,18 @@ and resources from servers. Additionally, handlers for these features may
themselves be stateful, for example if a tool handler caches state from earlier
requests in the session.
We believe that in the common case, any change to a client or server,
such as adding a tool, is intended for all its peers.
It is therefore more useful to allow multiple connections from a client, and to
a server. This is similar to the `net/http` packages, in which an `http.Client`
and `http.Server` each may handle multiple unrelated connections. When users
add features to a client or server, all connected peers are notified of the
change.
We believe that in the common case, any change to a client or server, such as
adding a tool, is intended for all its peers. It is therefore more useful to
allow multiple connections from a client, and to a server. This is similar to
the `net/http` packages, in which an `http.Client` and `http.Server` each may
handle multiple unrelated connections. When users add features to a client or
server, all connected peers are notified of the change.
Supporting multiple connections to servers (and from clients) still allows for
stateful components, as it is up to the user to decide whether or not to create
distinct servers/clients for each connection. For example, if the user wants to
create a distinct server for each new connection, they can do so in the
`getServer` factory passed to transport handlers.
Following the terminology of the
[spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management),
@ -423,6 +447,7 @@ func (*ClientSession) Client() *Client
func (*ClientSession) Close() error
func (*ClientSession) Wait() error
// Methods for calling through the ClientSession are described below.
// For example: ClientSession.ListTools.
type Server struct { /* ... */ }
func NewServer(name, version string, opts *ServerOptions) *Server
@ -436,6 +461,7 @@ func (*ServerSession) Server() *Server
func (*ServerSession) Close() error
func (*ServerSession) Wait() error
// Methods for calling through the ServerSession are described below.
// For example: ServerSession.ListRoots.
```
Here's an example of these APIs from the client side:
@ -472,17 +498,18 @@ session until the client disconnects:
func (*Server) Run(context.Context, Transport)
```
**Differences from mcp-go**: the Server APIs are very similar to mcp-go,
though the association between servers and transports is different. In
mcp-go, a single server is bound to what we would call an `SSEHTTPHandler`,
and reused for all client sessions. As discussed above, the transport
abstraction here is differentiated from HTTP serving, and the `Server.Connect`
method provides a consistent API for binding to an arbitrary transport. Servers
here do not have methods for sending notifications or calls, because they are
logically distinct from the `ServerSession`. In mcp-go, servers are `n:1`,
but there is no abstraction of a server session: sessions are addressed in
Server APIs through their `sessionID`: `SendNotificationToAllClients`,
`SendNotificationToClient`, `SendNotificationToSpecificClient`.
**Differences from mcp-go**: the Server APIs are similar to mcp-go, though the
association between servers and transports is different. In mcp-go, a single
server is bound to what we would call an `SSEHTTPHandler`, and reused for all
sessions. Per-session behavior is implemented though a 'session tool' overlay.
As discussed above, the transport abstraction here is differentiated from HTTP
serving, and the `Server.Connect` method provides a consistent API for binding
to an arbitrary transport. Servers here do not have methods for sending
notifications or calls, because they are logically distinct from the
`ServerSession`. In mcp-go, servers are `n:1`, but there is no abstraction of a
server session: sessions are addressed in Server APIs through their
`sessionID`: `SendNotificationToAllClients`, `SendNotificationToClient`,
`SendNotificationToSpecificClient`.
The client API here is different, since clients and client sessions are
conceptually distinct. The `ClientSession` is closer to mcp-go's notion of
@ -503,27 +530,25 @@ func (*ClientSession) ListTools(context.Context, *ListToolsParams) (*ListToolsRe
```
Our SDK has a method for every RPC in the spec, and except for `CallTool`,
their signatures all share this form.
We do this, rather than providing more convenient shortcut signatures,
to maintain backward compatibility if the spec makes backward-compatible changes
such as adding a new property to the request parameters
their signatures all share this form. We do this, rather than providing more
convenient shortcut signatures, to maintain backward compatibility if the spec
makes backward-compatible changes such as adding a new property to the request
parameters
(as in [this commit](https://github.com/modelcontextprotocol/modelcontextprotocol/commit/2fce8a077688bf8011e80af06348b8fe1dae08ac),
for example).
To avoid boilerplate, we don't repeat this
signature for RPCs defined in the spec; readers may assume it when we mention a
"spec method."
To avoid boilerplate, we don't repeat this signature for RPCs defined in the
spec; readers may assume it when we mention a "spec method."
`CallTool` is the only exception: for convenience, it takes the tool name and
arguments, with an options struct for additional request fields.
Our SDK has a method for every RPC in the spec, and their signatures all share
this form. To avoid boilerplate, we don't repeat this signature for RPCs
defined in the spec; readers may assume it when we mention a "spec method."
arguments, with an options struct for additional request fields. See the
section on Tools below for details.
Why do we use params instead of the full request? JSON-RPC requests consist of a method
name and a set of parameters, and the method is already encoded in the Go method name.
Technically, the MCP spec could add a field to a request while preserving backward
compatibility, which would break the Go SDK's compatibility. But in the unlikely event
that were to happen, we would add that field to the Params struct.
Why do we use params instead of the full JSON-RPC request? As much as possible,
we endeavor to hide JSON-RPC details when they are not relevant to the business
logic of your client or server. In this case, the additional information in the
JSON-RPC request is just the request ID and method name; the request ID is
irrelevant, and the method name is implied by the name of the Go method
providing the API.
We believe that any change to the spec that would require callers to pass a new a
parameter is not backward compatible. Therefore, it will always work to pass
@ -595,9 +620,9 @@ code.
```go
type JSONRPCError struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
Code int64 `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
}
```
@ -658,9 +683,9 @@ func (c *ServerSession) Ping(ctx context.Context, *PingParams) error
Additionally, client and server sessions can be configured with automatic
keepalive behavior. If the `KeepAlive` option is set to a non-zero duration,
it defines an
interval for regular "ping" requests. If the peer fails to respond to pings
originating from the keepalive check, the session is automatically closed.
it defines an interval for regular "ping" requests. If the peer fails to
respond to pings originating from the keepalive check, the session is
automatically closed.
```go
type ClientOptions struct {
@ -847,6 +872,19 @@ directly with assignment or a struct literal.
Client sessions can call the spec method `ListTools` or an iterator method `Tools`
to list the available tools.
As mentioned above, the client session method `CallTool` has a non-standard
signature, so that `CallTool` can handle the marshalling of tool arguments: the
type of `CallToolParams.Arguments` is `json.RawMessage`, to delegate
unmarshalling to the tool handler.
```go
func (c *ClientSession) CallTool(ctx context.Context, name string, args map[string]any, opts *CallToolOptions) (_ *CallToolResult, err error)
type CallToolOptions struct {
ProgressToken any // string or int
}
```
**Differences from mcp-go**: using variadic options to configure tools was
significantly inspired by mcp-go. However, the distinction between `ToolOption`
and `SchemaOption` allows for recursive application of schema options.
@ -972,6 +1010,11 @@ s.AddResources(&mcp.ServerResource{
Server sessions also support the spec methods `ListResources` and `ListResourceTemplates`,
and the corresponding iterator methods `Resources` and `ResourceTemplates`.
**Differences from mcp-go**: for symmetry with tools and prompts, we use
`AddResources` rather than `AddResource`. Additionally, the `ResourceHandler`
returns a `ReadResourceResult`, rather than just its content, for compatibility
with future evolution of the spec.
#### Subscriptions
ClientSessions can manage change notifications on particular resources:
@ -1105,3 +1148,7 @@ more pages exist.
In addition to the `List` methods, the SDK provides an iterator method for each
list operation. This simplifies pagination for clients by automatically handling
the underlying pagination logic. See [Iterator Methods](#iterator-methods) above.
**Differences with mcp-go**: the PageSize configuration is set with a
configuration field rather than a variadic option. Additionally, this design
proposes pagination by default, as this is likely desirable for most servers.

View file

@ -208,7 +208,7 @@ func (c *ClientSession) CallTool(ctx context.Context, name string, args map[stri
// NOTE: the following struct should consist of all fields of callToolParams except name and arguments.
// CallToolOptions contains options to [Client.CallTools].
// CallToolOptions contains options to [ClientSession.CallTool].
type CallToolOptions struct {
ProgressToken any // string or int
}

View file

@ -10,13 +10,13 @@
// To get started, create either a [Client] or [Server], and connect it to a
// peer using a [Transport]. The diagram below illustrates how this works:
//
// Client Server
// ⇅ (jsonrpc2) ⇅
// Client Transport ⇄ Server Transport ⇄ ServerSession
// Client Server
// ⇅ (jsonrpc2) ⇅
// ClientSession ⇄ Client Transport ⇄ Server Transport ⇄ ServerSession
//
// A [Client] is an MCP client, which can be configured with various client
// capabilities. Clients may be connected to a [Server] instance
// using the [Client.Start] method.
// using the [Client.Connect] method.
//
// Similarly, a [Server] is an MCP server, which can be configured with various
// server capabilities. Servers may be connected to one or more [Client]

View file

@ -20,8 +20,8 @@ type PromptHandler func(context.Context, *ServerSession, map[string]string) (*Ge
// A Prompt is a prompt definition bound to a prompt handler.
type ServerPrompt struct {
Definition *Prompt
Handler PromptHandler
Prompt *Prompt
Handler PromptHandler
}
// NewPrompt is a helper to use reflection to create a prompt for the given
@ -41,7 +41,7 @@ func NewPrompt[TReq any](name, description string, handler func(context.Context,
panic(fmt.Sprintf("handler request type must be a struct"))
}
prompt := &ServerPrompt{
Definition: &Prompt{
Prompt: &Prompt{
Name: name,
Description: description,
},
@ -54,7 +54,7 @@ func NewPrompt[TReq any](name, description string, handler func(context.Context,
if prop.Type != "string" {
panic(fmt.Sprintf("handler type must consist only of string fields"))
}
prompt.Definition.Arguments = append(prompt.Definition.Arguments, &PromptArgument{
prompt.Prompt.Arguments = append(prompt.Prompt.Arguments, &PromptArgument{
Name: name,
Description: prop.Description,
Required: required[name],
@ -95,16 +95,16 @@ func (s promptSetter) set(p *ServerPrompt) { s(p) }
// Required and Description, and panics when encountering any other option.
func Argument(name string, opts ...SchemaOption) PromptOption {
return promptSetter(func(p *ServerPrompt) {
i := slices.IndexFunc(p.Definition.Arguments, func(arg *PromptArgument) bool {
i := slices.IndexFunc(p.Prompt.Arguments, func(arg *PromptArgument) bool {
return arg.Name == name
})
var arg *PromptArgument
if i < 0 {
i = len(p.Definition.Arguments)
i = len(p.Prompt.Arguments)
arg = &PromptArgument{Name: name}
p.Definition.Arguments = append(p.Definition.Arguments, arg)
p.Prompt.Arguments = append(p.Prompt.Arguments, arg)
} else {
arg = p.Definition.Arguments[i]
arg = p.Prompt.Arguments[i]
}
for _, opt := range opts {
switch v := opt.(type) {
@ -116,6 +116,6 @@ func Argument(name string, opts ...SchemaOption) PromptOption {
panic(fmt.Sprintf("unsupported prompt argument schema option %T", opt))
}
}
p.Definition.Arguments[i] = arg
p.Prompt.Arguments[i] = arg
})
}

View file

@ -46,8 +46,8 @@ func TestNewPrompt(t *testing.T) {
},
}
for _, test := range tests {
if diff := cmp.Diff(test.want, test.prompt.Definition.Arguments); diff != "" {
t.Errorf("NewPrompt(%v) mismatch (-want +got):\n%s", test.prompt.Definition.Name, diff)
if diff := cmp.Diff(test.want, test.prompt.Prompt.Arguments); diff != "" {
t.Errorf("NewPrompt(%v) mismatch (-want +got):\n%s", test.prompt.Prompt.Name, diff)
}
}
}

View file

@ -53,8 +53,8 @@ func NewServer(name, version string, opts *ServerOptions) *Server {
name: name,
version: version,
opts: *opts,
prompts: newFeatureSet(func(p *ServerPrompt) string { return p.Definition.Name }),
tools: newFeatureSet(func(t *ServerTool) string { return t.Definition.Name }),
prompts: newFeatureSet(func(p *ServerPrompt) string { return p.Prompt.Name }),
tools: newFeatureSet(func(t *ServerTool) string { return t.Tool.Name }),
resources: newFeatureSet(func(r *ServerResource) string { return r.Resource.URI }),
}
}
@ -170,7 +170,7 @@ func (s *Server) listPrompts(_ context.Context, _ *ServerSession, params *ListPr
defer s.mu.Unlock()
res := new(ListPromptsResult)
for p := range s.prompts.all() {
res.Prompts = append(res.Prompts, p.Definition)
res.Prompts = append(res.Prompts, p.Prompt)
}
return res, nil
}
@ -191,7 +191,7 @@ func (s *Server) listTools(_ context.Context, _ *ServerSession, params *ListTool
defer s.mu.Unlock()
res := new(ListToolsResult)
for t := range s.tools.all() {
res.Tools = append(res.Tools, t.Definition)
res.Tools = append(res.Tools, t.Tool)
}
return res, nil
}

View file

@ -17,8 +17,8 @@ type ToolHandler func(context.Context, *ServerSession, *CallToolParams) (*CallTo
// A Tool is a tool definition that is bound to a tool handler.
type ServerTool struct {
Definition *Tool
Handler ToolHandler
Tool *Tool
Handler ToolHandler
}
// NewTool is a helper to make a tool using reflection on the given handler.
@ -59,7 +59,7 @@ func NewTool[TReq any](name, description string, handler func(context.Context, *
return res, nil
}
t := &ServerTool{
Definition: &Tool{
Tool: &Tool{
Name: name,
Description: description,
InputSchema: schema,
@ -94,7 +94,7 @@ func (s toolSetter) set(t *ServerTool) { s(t) }
func Input(opts ...SchemaOption) ToolOption {
return toolSetter(func(t *ServerTool) {
for _, opt := range opts {
opt.set(t.Definition.InputSchema)
opt.set(t.Tool.InputSchema)
}
})
}

View file

@ -83,8 +83,8 @@ func TestNewTool(t *testing.T) {
},
}
for _, test := range tests {
if diff := cmp.Diff(test.want, test.tool.Definition.InputSchema, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
t.Errorf("NewTool(%v) mismatch (-want +got):\n%s", test.tool.Definition.Name, diff)
if diff := cmp.Diff(test.want, test.tool.Tool.InputSchema, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
t.Errorf("NewTool(%v) mismatch (-want +got):\n%s", test.tool.Tool.Name, diff)
}
}
}