internal/mcp/design: remove the protocol package; update tools

Per discussion, remove the separate protocol package in favor of a
single mcp package. Update the tools section accordingly, and add more
discussion about the decision of the NewTool API.

Change-Id: Ifb259555ca9d493ce63bc4f71e5f2fa295bdf09c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/671496
TryBot-Bypass: Robert Findley <rfindley@google.com>
Commit-Queue: Robert Findley <rfindley@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/mcp/design/design.md b/internal/mcp/design/design.md
index 3f1e5f7..1bb65e5 100644
--- a/internal/mcp/design/design.md
+++ b/internal/mcp/design/design.md
@@ -60,14 +60,9 @@
 final module path of the mcp module
 
 - `module.path/mcp`: the bulk of the user facing API
-- `module.path/mcp/protocol`: generated types for the MCP spec.
 - `module.path/jsonschema`: a jsonschema implementation, with validation
 - `module.path/internal/jsonrpc2`: a fork of x/tools/internal/jsonrpc2_v2
 
-For now, this layout assumes we want to separate the 'protocol' types from the
-'mcp' package, since they won't be needed by most users. It is unclear whether
-this is worthwhile.
-
 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
@@ -118,7 +113,7 @@
 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
 logical stream, and the lower-level interface is easier to implement in most
-cases, which means it is easier to implement custom transports or middleware.
+cases, which means it is easier to implement custom transports.
 
 For our prototype, we've used an internal `jsonrpc2` package based on the Go
 language server `gopls`, which we propose to fork for the MCP SDK. It already
@@ -268,13 +263,15 @@
 
 ### Protocol types
 
-As described in the section on package layout above, the `protocol` package
-will contain definitions of types referenced by the MCP spec that are needed
-for the SDK. JSON-RPC message types are elided, since they are handled by the
-`jsonrpc2` package and should not be observed by the user. The user interacts
-only with the params/result types relevant to MCP operations.
+Types needed for the protocol are generated from the
+[JSON schema of the MCP spec](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json).
 
-For user-provided data, use `json.RawMessage`, so that
+These types will be included in the `mcp` package, but will be unexported
+unless they are needed for the user-facing API. Notably, JSON-RPC message types
+are elided, since they are handled by the `jsonrpc2` package and should not be
+observed by the user.
+
+For user-provided data, we use `json.RawMessage`, so that
 marshalling/unmarshalling can be delegated to the business logic of the client
 or server.
 
@@ -282,8 +279,6 @@
 `Resource`), we prefer distinguished unions: struct types with fields
 corresponding to the union of all properties for union elements.
 
-These types will be auto-generated from the [JSON schema of the MCP
-spec](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json).
 For brevity, only a few examples are shown here:
 
 ```go
@@ -529,9 +524,9 @@
 func (*Client) RemoveRoots(roots ...string)
 ```
 
-Servers can call `ListRoots` to get the roots.
-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
+Servers can call `ListRoots` to get the roots. 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
@@ -546,9 +541,8 @@
 
 ### Sampling
 
-Clients that support sampling are created with a `CreateMessageHandler` option for handling server
-calls.
-To perform sampling, a server calls `CreateMessage`.
+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 {
@@ -563,6 +557,25 @@
 
 ### Tools
 
+A `Tool` is a logical MCP tool, generated from the MCP spec, and a `ServerTool`
+is a tool bound to a tool handler.
+
+```go
+type Tool struct {
+	Annotations *ToolAnnotations   `json:"annotations,omitempty"`
+	Description string             `json:"description,omitempty"`
+	InputSchema *jsonschema.Schema `json:"inputSchema"`
+	Name string                    `json:"name"`
+}
+
+type ToolHandler func(context.Context, *ServerSession, map[string]json.RawMessage) (*CallToolResult, error)
+
+type ServerTool struct {
+	Tool    Tool
+	Handler ToolHandler
+}
+```
+
 Add tools to a server with `AddTools`:
 
 ```go
@@ -573,34 +586,44 @@
 
 Remove them by name with `RemoveTools`:
 
-```
-  server.RemoveTools("add", "subtract")
+```go
+server.RemoveTools("add", "subtract")
 ```
 
-We provide a convenient and type-safe way to construct a Tool:
+A tool's input schema, expressed as a [JSON Schema](https://json-schema.org),
+provides a way to validate the tool's input. One of the challenges in defining
+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
+approaches:
+
+1. Use reflection to generate the tool's input schema from a Go type (ala
+   `metoro-io/mcp-golang`)
+2. Explicitly build the input schema (ala `mark3labs/mcp-go`).
+
+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
+schema of your API is compatible with your Go types by construction. It also
+means that concerns like parsing and validation can be handled automatically.
+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
+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
+involve significant boilerplate.
+
+We believe that a hybrid model works well, where the _initial_ 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
+generates the schema from the input type, and wraps the handler to provide
+parsing and validation. The schema (and potentially other features) can be
+customized using ToolOptions.
 
 ```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
+func NewTool[TInput any](name, description string, handler func(context.Context, *ServerSession, TInput) ([]Content, error), opts …ToolOption) *ServerTool
+
+type ToolOption interface { /* ... */ }
 ```
 
-The `TReq` type is typically a struct, and we use reflection on the struct to
-determine the names and types of the tool's input. `ToolOption`s allow further
-customization of a Tool's input schema.
-Since all the fields of the Tool struct are exported, a Tool can also be created
-directly with assignment or a struct literal.
-
-A tool's input schema, expressed as a [JSON Schema](https://json-schema.org),
-provides a way to validate the tool's input.
-
-We chose a hybrid a approach to specifying the schema, combining reflection
-and variadic options. We found that this makes the common cases easy (sometimes
-free!) to express and keeps the API small. The most recent JSON Schema
-spec defines over 40 keywords. Providing them all as options would bloat
-the API despite the fact that most would be very rarely used. Our approach
-also guarantees that the input schema is compatible with tool parameters, by
-construction.
-
 `NewTool` determines the input schema for a Tool from the struct used
 in the handler. Each struct field that would be marshaled by `encoding/json.Marshal`
 becomes a property of the schema. The property is required unless
@@ -618,23 +641,40 @@
 
 "name" and "Choices" are required, while "count" is optional.
 
-The struct provides the names, types and required status of the properties.
-Other JSON Schema keywords can be specified by passing options to `NewTool`:
+As of writing, the only `ToolOption` is `Input`, which allows customizing the
+input schema of the tool using schema options. These schema options are
+recursive, in the sense that they may also be applied to properties.
+
+```go
+func Input(...SchemaOption) ToolOption
+
+type Property(name string, opts ...SchemaOption) SchemaOption
+type Description(desc string) SchemaOption
+// etc.
+```
+
+For example:
 
 ```go
 NewTool(name, description, handler,
     Input(Property("count", Description("size of the inventory"))))
 ```
 
-For less common keywords, use the `Schema` option:
+The most recent JSON Schema spec defines over 40 keywords. Providing them all
+as options would bloat the API despite the fact that most would be very rarely
+used. For less common keywords, use the `Schema` option to set the schema
+explicitly:
 
-```
+```go
 NewTool(name, description, handler,
     Input(Property("Choices", Schema(&jsonschema.Schema{UniqueItems: true}))))
 ```
 
 Schemas are validated on the server before the tool handler is called.
 
+Since all the fields of the Tool struct are exported, a Tool can also be created
+directly with assignment or a struct literal.
+
 ### Prompts
 
 Use `NewPrompt` to create a prompt.