internal/mcp/design.md: minor changes
Fix typos and formatting, reword slightly. Also, change names in the middleware section. Change-Id: Ie86e5f633a07675ab188a8aaf70e51a48b0878bb Reviewed-on: https://go-review.googlesource.com/c/tools/+/671555 Reviewed-by: Robert Findley <rfindley@google.com> TryBot-Bypass: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
parent
0f2d85336d
commit
b767218505
1 changed files with 37 additions and 35 deletions
|
|
@ -59,7 +59,7 @@ SDK. An official SDK should aim to be:
|
||||||
In the sections below, we visit each aspect of the MCP spec, in approximately
|
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)
|
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
|
For each, we discuss considerations for the Go implementation. In many cases an
|
||||||
API is suggested, though in some there many be open questions.
|
API is suggested, though in some there may be open questions.
|
||||||
|
|
||||||
## Foundations
|
## Foundations
|
||||||
|
|
||||||
|
|
@ -123,10 +123,11 @@ type Stream interface {
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Methods accept a Go `Context` and return an `error`,
|
||||||
|
as is idiomatic for APIs that do I/O.
|
||||||
|
|
||||||
Specifically, a `Transport` is something that connects a logical JSON-RPC
|
A `Transport` is something that connects a logical JSON-RPC
|
||||||
stream, and nothing more (methods accept a Go `Context` and return an `error`,
|
stream, and nothing more. Streams must be closeable in order to
|
||||||
as is idiomatic for APIs that do I/O). Streams must be closeable in order to
|
|
||||||
implement client and server shutdown, and therefore conform to the `io.Closer`
|
implement client and server shutdown, and therefore conform to the `io.Closer`
|
||||||
interface.
|
interface.
|
||||||
|
|
||||||
|
|
@ -202,9 +203,10 @@ func (*SSEHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||||
func (*SSEHTTPHandler) Close() error
|
func (*SSEHTTPHandler) Close() error
|
||||||
```
|
```
|
||||||
|
|
||||||
Notably absent are options to hook into the request handling for the purposes
|
Notably absent are options to hook into low-level request handling for the purposes
|
||||||
of authentication or context injection. These concerns are better handled using
|
of authentication or context injection. These concerns are better handled using
|
||||||
standard HTTP middleware patterns.
|
standard HTTP middleware patterns. For middleware at the level of the MCP protocol,
|
||||||
|
see [Middleware](#Middleware) below.
|
||||||
|
|
||||||
By default, the SSE handler creates messages endpoints with the
|
By default, the SSE handler creates messages endpoints with the
|
||||||
`?sessionId=...` query parameter. Users that want more control over the
|
`?sessionId=...` query parameter. Users that want more control over the
|
||||||
|
|
@ -247,9 +249,7 @@ type SSEClientTransport struct { /* ... */ }
|
||||||
|
|
||||||
// NewSSEClientTransport returns a new client transport that connects to the
|
// NewSSEClientTransport returns a new client transport that connects to the
|
||||||
// SSE server at the provided URL.
|
// SSE server at the provided URL.
|
||||||
//
|
func NewSSEClientTransport(url string) (*SSEClientTransport, error) {
|
||||||
// NewSSEClientTransport panics if the given URL is invalid.
|
|
||||||
func NewSSEClientTransport(url string) *SSEClientTransport {
|
|
||||||
|
|
||||||
// Connect connects through the client endpoint.
|
// Connect connects through the client endpoint.
|
||||||
func (*SSEClientTransport) Connect(ctx context.Context) (Stream, error)
|
func (*SSEClientTransport) Connect(ctx context.Context) (Stream, error)
|
||||||
|
|
@ -378,12 +378,13 @@ and resources from servers. Additionally, handlers for these features may
|
||||||
themselves be stateful, for example if a tool handler caches state from earlier
|
themselves be stateful, for example if a tool handler caches state from earlier
|
||||||
requests in the session.
|
requests in the session.
|
||||||
|
|
||||||
We believe that in the common case, both clients and servers are stateless, and
|
We believe that in the common case, any change to a client or server,
|
||||||
it is therefore more useful to allow multiple connections from a client, and to
|
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`
|
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
|
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
|
add features to a client or server, all connected peers are notified of the
|
||||||
change in feature-set.
|
change.
|
||||||
|
|
||||||
Following the terminology of the
|
Following the terminology of the
|
||||||
[spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management),
|
[spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management),
|
||||||
|
|
@ -435,9 +436,7 @@ client := mcp.NewClient("mcp-client", "v1.0.0", nil)
|
||||||
// Connect to a server over stdin/stdout
|
// Connect to a server over stdin/stdout
|
||||||
transport := mcp.NewCommandTransport(exec.Command("myserver"))
|
transport := mcp.NewCommandTransport(exec.Command("myserver"))
|
||||||
session, err := client.Connect(ctx, transport)
|
session, err := client.Connect(ctx, transport)
|
||||||
if err != nil {
|
if err != nil { ... }
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// Call a tool on the server.
|
// Call a tool on the server.
|
||||||
content, err := session.CallTool(ctx, "greet", map[string]any{"name": "you"})
|
content, err := session.CallTool(ctx, "greet", map[string]any{"name": "you"})
|
||||||
...
|
...
|
||||||
|
|
@ -491,12 +490,14 @@ We provide a mechanism to add MCP-level middleware, which runs after the
|
||||||
request has been parsed, but before any normal handling.
|
request has been parsed, but before any normal handling.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// A Handler handles an MCP message call.
|
// A Dispatcher dispatches an MCP message to the appropriate handler.
|
||||||
type Handler func(ctx context.Context, s *ServerSession, method string, params any) (response any, err error)
|
// The params argument will be an XXXParams struct pointer, such as *GetPromptParams.
|
||||||
|
// The response if err is non-nil should be an XXXResult struct pointer.
|
||||||
|
type Dispatcher func(ctx context.Context, s *ServerSession, method string, params any) (result any, err error)
|
||||||
|
|
||||||
// AddMiddleware calls each middleware function from right to left on the previous result, beginning
|
// AddDispatchers calls each function from right to left on the previous result, beginning
|
||||||
// with the server's current handler, and installs the result as the new handler.
|
// with the server's current dispatcher, and installs the result as the new handler.
|
||||||
func (*Server) AddMiddleware(middleware ...func(Handler) Handler))
|
func (*Server) AddDispatchers(middleware ...func(Handler) Handler))
|
||||||
```
|
```
|
||||||
|
|
||||||
As an example, this code adds server-side logging:
|
As an example, this code adds server-side logging:
|
||||||
|
|
@ -510,7 +511,7 @@ func withLogging(h mcp.Handler) mcp.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.AddMiddleware(withLogging)
|
server.AddDispatchers(withLogging)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Differences from mcp-go**: Version 0.26.0 of mcp-go defines 24 server hooks.
|
**Differences from mcp-go**: Version 0.26.0 of mcp-go defines 24 server hooks.
|
||||||
|
|
@ -558,14 +559,14 @@ When this client call is cancelled, a `"notifications/cancelled"` notification
|
||||||
is sent to the server. However, the client call returns immediately with
|
is sent to the server. However, the client call returns immediately with
|
||||||
`ctx.Err()`: it does not wait for the result from the server.
|
`ctx.Err()`: it does not wait for the result from the server.
|
||||||
|
|
||||||
The server observes a client cancellation as cancelled context.
|
The server observes a client cancellation as a cancelled context.
|
||||||
|
|
||||||
### Progress handling
|
### Progress handling
|
||||||
|
|
||||||
A caller can request progress notifications by setting the `ProgressToken` field on any request.
|
A caller can request progress notifications by setting the `ProgressToken` field on any request.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type ProgressToken any
|
type ProgressToken any // string or int
|
||||||
|
|
||||||
type XXXParams struct { // where XXX is each type of call
|
type XXXParams struct { // where XXX is each type of call
|
||||||
...
|
...
|
||||||
|
|
@ -595,7 +596,8 @@ func (c *ServerSession) Ping(ctx context.Context) error
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, client and server sessions can be configured with automatic
|
Additionally, client and server sessions can be configured with automatic
|
||||||
keepalive behavior. If set to a non-zero value, this duration defines an
|
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
|
interval for regular "ping" requests. If the peer fails to respond to pings
|
||||||
originating from the keepalive check, the session is automatically closed.
|
originating from the keepalive check, the session is automatically closed.
|
||||||
|
|
||||||
|
|
@ -707,9 +709,9 @@ tools is the need to associate them with a Go function, yet support the
|
||||||
arbitrary complexity of JSON Schema. To achieve this, we have seen two primary
|
arbitrary complexity of JSON Schema. To achieve this, we have seen two primary
|
||||||
approaches:
|
approaches:
|
||||||
|
|
||||||
1. Use reflection to generate the tool's input schema from a Go type (ala
|
1. Use reflection to generate the tool's input schema from a Go type (à la
|
||||||
`metoro-io/mcp-golang`)
|
`metoro-io/mcp-golang`)
|
||||||
2. Explicitly build the input schema (ala `mark3labs/mcp-go`).
|
2. Explicitly build the input schema (à la `mark3labs/mcp-go`).
|
||||||
|
|
||||||
Both of these have their advantages and disadvantages. Reflection is nice,
|
Both of these have their advantages and disadvantages. Reflection is nice,
|
||||||
because it allows you to bind directly to a Go API, and means that the JSON
|
because it allows you to bind directly to a Go API, and means that the JSON
|
||||||
|
|
@ -718,10 +720,10 @@ means that concerns like parsing and validation can be handled automatically.
|
||||||
However, it can become cumbersome to express the full breadth of JSON schema
|
However, it can become cumbersome to express the full breadth of JSON schema
|
||||||
using Go types or struct tags, and sometimes you want to express things that
|
using Go types or struct tags, and sometimes you want to express things that
|
||||||
aren’t naturally modeled by Go types, like unions. Explicit schemas are simple
|
aren’t naturally modeled by Go types, like unions. Explicit schemas are simple
|
||||||
and readable, and gives the caller full control over their tool definition, but
|
and readable, and give the caller full control over their tool definition, but
|
||||||
involve significant boilerplate.
|
involve significant boilerplate.
|
||||||
|
|
||||||
We believe that a hybrid model works well, where the _initial_ schema is
|
We have found that a hybrid model works well, where the _initial_ schema is
|
||||||
derived using reflection, but any customization on top of that schema is
|
derived using reflection, but any customization on top of that schema is
|
||||||
applied using variadic options. We achieve this using a `NewTool` helper, which
|
applied using variadic options. We achieve this using a `NewTool` helper, which
|
||||||
generates the schema from the input type, and wraps the handler to provide
|
generates the schema from the input type, and wraps the handler to provide
|
||||||
|
|
@ -729,7 +731,7 @@ parsing and validation. The schema (and potentially other features) can be
|
||||||
customized using ToolOptions.
|
customized using ToolOptions.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// NewTool is a creates a Tool using reflection on the given handler.
|
// NewTool creates a Tool using reflection on the given handler.
|
||||||
func NewTool[TInput any](name, description string, handler func(context.Context, *ServerSession, TInput) ([]Content, error), opts …ToolOption) *ServerTool
|
func NewTool[TInput any](name, description string, handler func(context.Context, *ServerSession, TInput) ([]Content, error), opts …ToolOption) *ServerTool
|
||||||
|
|
||||||
type ToolOption interface { /* ... */ }
|
type ToolOption interface { /* ... */ }
|
||||||
|
|
@ -752,7 +754,7 @@ struct {
|
||||||
|
|
||||||
"name" and "Choices" are required, while "count" is optional.
|
"name" and "Choices" are required, while "count" is optional.
|
||||||
|
|
||||||
As of writing, the only `ToolOption` is `Input`, which allows customizing the
|
As of this writing, the only `ToolOption` is `Input`, which allows customizing the
|
||||||
input schema of the tool using schema options. These schema options are
|
input schema of the tool using schema options. These schema options are
|
||||||
recursive, in the sense that they may also be applied to properties.
|
recursive, in the sense that they may also be applied to properties.
|
||||||
|
|
||||||
|
|
@ -840,7 +842,7 @@ server.AddPrompts(
|
||||||
server.RemovePrompts("code_review")
|
server.RemovePrompts("code_review")
|
||||||
```
|
```
|
||||||
|
|
||||||
Clients can call ListPrompts to list the available prompts and GetPrompt to get one.
|
Clients can call `ListPrompts` to list the available prompts and `GetPrompt` to get one.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (*ClientSession) ListPrompts(context.Context, *ListPromptParams) (*ListPromptsResult, error)
|
func (*ClientSession) ListPrompts(context.Context, *ListPromptParams) (*ListPromptsResult, error)
|
||||||
|
|
@ -862,8 +864,8 @@ func (*Server) AddResourceTemplates(templates...*ResourceTemplate)
|
||||||
func (*Server) RemoveResourceTemplates(names ...string)
|
func (*Server) RemoveResourceTemplates(names ...string)
|
||||||
```
|
```
|
||||||
|
|
||||||
Clients call ListResources to list the available resources, ReadResource to read
|
Clients call `ListResources` to list the available resources, `ReadResource` to read
|
||||||
one of them, and ListResourceTemplates to list the templates:
|
one of them, and `ListResourceTemplates` to list the templates:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (*ClientSession) ListResources(context.Context, *ListResourcesParams) (*ListResourcesResult, error)
|
func (*ClientSession) ListResources(context.Context, *ListResourcesParams) (*ListResourcesResult, error)
|
||||||
|
|
@ -909,7 +911,7 @@ defined its server-side behavior.
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
Servers have access to a `slog.Logger` that writes to the client. A call to
|
ServerSessions have access to a `slog.Logger` that writes to the client. A call to
|
||||||
a log method like `Info`is translated to a `LoggingMessageNotification` as
|
a log method like `Info`is translated to a `LoggingMessageNotification` as
|
||||||
follows:
|
follows:
|
||||||
|
|
||||||
|
|
@ -924,7 +926,7 @@ follows:
|
||||||
to integers between the slog levels. For example, "notice" is level 2 because
|
to integers between the slog levels. For example, "notice" is level 2 because
|
||||||
it is between "warning" (slog value 4) and "info" (slog value 0).
|
it is between "warning" (slog value 4) and "info" (slog value 0).
|
||||||
The `mcp` package defines consts for these levels. To log at the "notice"
|
The `mcp` package defines consts for these levels. To log at the "notice"
|
||||||
level, a server would call `Log(ctx, mcp.LevelNotice, "message")`.
|
level, a handler would call `session.Log(ctx, mcp.LevelNotice, "message")`.
|
||||||
|
|
||||||
### Pagination
|
### Pagination
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue