internal/design: minor cleanup; add 'errors' and 'ping' sections
Perform some minor cleanup:
- Resolve semantic conflicts updating ServerConnection->ServerSession
- Use 'go' code blocks throughout
Also, add brief sections discussing error handling and 'ping/keepalive'.
Change-Id: I9ee888a7f0506ca380c2d4d4f9965a4c399aac19
Reviewed-on: https://go-review.googlesource.com/c/tools/+/671495
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Commit-Queue: Robert Findley <rfindley@google.com>
TryBot-Bypass: Robert Findley <rfindley@google.com>
diff --git a/internal/mcp/design/design.md b/internal/mcp/design/design.md
index e37604a..3f1e5f7 100644
--- a/internal/mcp/design/design.md
+++ b/internal/mcp/design/design.md
@@ -362,7 +362,10 @@
func (c *Client) Connect(context.Context, Transport) (*ClientSession, error)
// Methods for adding/removing client features are described below.
+type ClientOptions struct { /* ... */ } // described below
+
type ClientSession struct { /* ... */ }
+func (*ClientSession) Client() *Client
func (*ClientSession) Close() error
func (*ClientSession) Wait() error
// Methods for calling through the ClientSession are described below.
@@ -372,13 +375,16 @@
func (s *Server) Connect(context.Context, Transport) (*ServerSession, error)
// Methods for adding/removing server features are described below.
+type ServerOptions struct { /* ... */ } // described below
+
type ServerSession struct { /* ... */ }
+func (*ServerSession) Server() *Server
func (*ServerSession) Close() error
func (*ServerSession) Wait() error
// Methods for calling through the ServerSession are described below.
```
-Here's an example of these API from the client side:
+Here's an example of these APIs from the client side:
```go
client := mcp.NewClient("mcp-client", "v1.0.0", nil)
@@ -416,7 +422,24 @@
### Errors
-<!-- TODO: a brief section discussing how errors are handled. -->
+With the exception of tool handler errors, protocol errors are handled
+transparently as Go errors: errors in server-side feature handlers are
+propagated as errors from calls from the `ClientSession`, and vice-versa.
+
+Protocol errors wrap a `JSONRPC2Error` type which exposes its underlying error
+code.
+
+```go
+type JSONRPC2Error struct {
+ Code int64 `json:"code"`
+ Message string `json:"message"`
+ Data json.RawMessage `json:"data,omitempty"`
+}
+```
+
+As described by the
+[spec](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#error-handling),
+tool execution errors are reported in tool results.
### Cancellation
@@ -438,6 +461,7 @@
### Progress handling
A caller can request progress notifications by setting the `ProgressToken` field on any request.
+
```go
type ProgressToken any
@@ -446,22 +470,44 @@
ProgressToken ProgressToken
}
```
+
Handlers can notify their peer about progress by calling the `NotifyProgress`
method. The notification is only sent if the peer requested it.
+
```go
func (*ClientSession) NotifyProgress(context.Context, *ProgressNotification)
func (*ServerSession) NotifyProgress(context.Context, *ProgressNotification)
```
+
We don't support progress notifications for `Client.ListRoots`, because we expect
that operation to be instantaneous relative to network latency.
+### Ping / KeepAlive
-### Ping / Keepalive
+Both `ClientSession` and `ServerSession` expose a `Ping` method to call "ping"
+on their peer.
-<!--
-TODO: discuss the implementation of 'ping', as well as APIs for
-parameterizing automatic keepalive.
--->
+```go
+func (c *ClientSession) Ping(ctx context.Context) error
+func (c *ServerSession) Ping(ctx context.Context) error
+```
+
+Additionally, client and server sessions can be configured with automatic
+keepalive behavior. If set to a non-zero value, this duration 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 {
+ ...
+ KeepAlive time.Duration
+}
+
+type ServerOptions struct {
+ ...
+ KeepAlive time.Duration
+}
+```
## Client Features
@@ -470,7 +516,7 @@
Clients support the MCP Roots feature out of the box, including roots-changed notifications.
Roots can be added and removed from a `Client` with `AddRoots` and `RemoveRoots`:
-```
+```go
// AddRoots adds the roots to the client's list of roots.
// If the list changes, the client notifies the server.
// If a root does not begin with a valid URI schema such as "https://" or "file://",
@@ -487,13 +533,14 @@
If a server installs a `RootsChangedHandler`, it will be called when the client sends a
roots-changed notification, which happens whenever the list of roots changes after a
connection has been established.
-```
+
+```go
func (*Server) ListRoots(context.Context, *ListRootsParams) (*ListRootsResult, error)
type ServerOptions {
...
// If non-nil, called when a client sends a roots-changed notification.
- RootsChangedHandler func(context.Context, *ServerConnection, *RootsChangedParams)
+ RootsChangedHandler func(context.Context, *ServerSession, *RootsChangedParams)
}
```
@@ -502,7 +549,8 @@
Clients that support sampling are created with a `CreateMessageHandler` option for handling server
calls.
To perform sampling, a server calls `CreateMessage`.
-```
+
+```go
type ClientOptions struct {
...
CreateMessageHandler func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error)
@@ -516,19 +564,22 @@
### Tools
Add tools to a server with `AddTools`:
-```
+
+```go
server.AddTools(
mcp.NewTool("add", "add numbers", addHandler),
mcp.NewTools("subtract, subtract numbers", subHandler))
```
+
Remove them by name with `RemoveTools`:
+
```
server.RemoveTools("add", "subtract")
```
We provide a convenient and type-safe way to construct a Tool:
-```
+```go
// NewTool is a creates a Tool using reflection on the given handler.
func NewTool[TReq any](name, description string, handler func(context.Context, TReq) ([]Content, error), opts …ToolOption) *Tool
```
@@ -556,7 +607,7 @@
the field's `json` tag specifies "omitempty" or "omitzero" (new in Go 1.24).
For example, given this struct:
-```
+```go
struct {
Name string `json:"name"`
Count int `json:"count,omitempty"`
@@ -570,7 +621,7 @@
The struct provides the names, types and required status of the properties.
Other JSON Schema keywords can be specified by passing options to `NewTool`:
-```
+```go
NewTool(name, description, handler,
Input(Property("count", Description("size of the inventory"))))
```
@@ -589,11 +640,13 @@
Use `NewPrompt` to create a prompt.
As with tools, prompt argument schemas can be inferred from a struct, or obtained
from options.
+
```go
func NewPrompt[TReq any](name, description string,
handler func(context.Context, *ServerSession, TReq) (*GetPromptResult, error),
opts ...PromptOption) *ServerPrompt
```
+
Use `AddPrompts` to add prompts to the server, and `RemovePrompts`
to remove them by name.
@@ -612,6 +665,7 @@
```
Clients can call ListPrompts to list the available prompts and GetPrompt to get one.
+
```go
func (*ClientSession) ListPrompts(context.Context, *ListPromptParams) (*ListPromptsResult, error)
func (*ClientSession) GetPrompt(context.Context, *GetPromptParams) (*GetPromptResult, error)
@@ -620,14 +674,17 @@
### Resources and resource templates
Servers have Add and Remove methods for resources and resource templates:
+
```go
func (*Server) AddResources(resources ...*Resource)
func (*Server) RemoveResources(names ...string)
func (*Server) AddResourceTemplates(templates...*ResourceTemplate)
func (*Server) RemoveResourceTemplates(names ...string)
```
+
Clients call ListResources to list the available resources, ReadResource to read
one of them, and ListResourceTemplates to list the templates:
+
```go
func (*ClientSession) ListResources(context.Context, *ListResourcesParams) (*ListResourcesResult, error)
func (*ClientSession) ReadResource(context.Context, *ReadResourceParams) (*ReadResourceResult, error)
@@ -642,6 +699,7 @@
or RemoveXXX call, the server informs all its connected clients by sending the
corresponding type of notification.
A client will receive these notifications if it was created with the corresponding option:
+
```go
type ClientOptions struct {
...
@@ -735,7 +793,7 @@
[this code](https://github.com/DCjanus/dida365-mcp-server/blob/master/cmd/mcp/tools.go#L315),
which must resort to untyped maps to express a nested schema:
-```
+```go
mcp.WithArray("items",
mcp.Description("Checklist items of the task"),
mcp.Items(map[string]any{
@@ -770,10 +828,9 @@
so instead we provide a single way to intercept this message handling, using
two exported names instead of 72:
-```
+```go
// A Handler handles an MCP message call.
-type Handler func(ctx context.Context, c *ServerConnection, method string, params any) (response any, err error)
-
+type Handler func(ctx context.Context, s *ServerSession, method string, params any) (response any, err error)
// AddMiddleware calls each middleware function from right to left on the previous result, beginning
// with the server's current handler, and installs the result as the new handler.
@@ -782,13 +839,12 @@
As an example, this code adds server-side logging:
-```
-
+```go
func withLogging(h mcp.Handler) mcp.Handler {
- return func(ctx context.Context, c *mcp.ServerConnection, method string, params any) (res any, err error) {
+ return func(ctx context.Context, s *mcp.ServerSession, method string, params any) (res any, err error) {
log.Printf("request: %s %v", method, params)
defer func() { log.Printf("response: %v, %v", res, err) }()
- return h(ctx, c , method, params)
+ return h(ctx, s , method, params)
}
}