mcp: add docs for tier 1 assessment (#1195)
Closes open points in [comment](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/3220#issuecomment-5373089321)
This commit is contained in:
parent
6f0a645c20
commit
b8c53fedd6
13 changed files with 1461 additions and 5 deletions
|
|
@ -21,6 +21,9 @@ transparently, while adhering to these goals.
|
|||
1. [Contributing code](#contributing-code)
|
||||
1. [Adding and updating dependencies](#adding-and-updating-dependencies)
|
||||
1. [Updating the README](#updating-the-readme)
|
||||
1. [Versioning](#versioning)
|
||||
1. [What counts as a breaking change](#what-counts-as-a-breaking-change)
|
||||
1. [Deprecation](#deprecation)
|
||||
1. [Timeouts](#timeouts)
|
||||
1. [Code of conduct](#code-of-conduct)
|
||||
1. [Governance](#governance)
|
||||
|
|
@ -188,6 +191,57 @@ The CI system will automatically check that the README is up-to-date by running
|
|||
`go generate ./internal/readme` and verifying no changes result. If you see a CI failure about the
|
||||
README being out of sync, follow the steps above to regenerate it.
|
||||
|
||||
## Versioning
|
||||
|
||||
The SDK follows [semantic versioning](https://semver.org), as the Go module
|
||||
system requires: the major version is part of the module path, so a breaking
|
||||
release would import as `github.com/modelcontextprotocol/go-sdk/v2`. A breaking
|
||||
change therefore cannot reach existing users by accident; they have to change
|
||||
their import path to receive one.
|
||||
|
||||
This policy covers the exported API of the SDK's importable packages — `mcp`,
|
||||
`jsonrpc`, `auth`, `auth/extauth` and `oauthex`. Everything under `internal/`
|
||||
is not importable outside the module and may change in any release.
|
||||
|
||||
Which MCP spec revisions each SDK version speaks is documented in the
|
||||
[README](README.md#version-compatibility). Support for a new revision arrives
|
||||
in a minor release; dropping a revision the SDK previously negotiated is a
|
||||
breaking change.
|
||||
|
||||
### What counts as a breaking change
|
||||
|
||||
- Removing or renaming an exported identifier.
|
||||
- Changing the signature of an exported function or method.
|
||||
- Removing or renaming a field of an exported struct, or changing its type.
|
||||
- Adding a method to an exported interface that users are meant to implement.
|
||||
- Changing documented behavior that callers could reasonably rely on.
|
||||
- Dropping support for an MCP spec revision.
|
||||
|
||||
The following are not breaking, and may land in a minor or patch release:
|
||||
|
||||
- Adding an exported identifier, or a field to an exported struct.
|
||||
- Fixing behavior that contradicted its documentation.
|
||||
- Refactoring that leaves the exported API unchanged.
|
||||
- Adding support for a new MCP spec revision.
|
||||
- Raising the minimum Go version to one still supported upstream, per the
|
||||
[README](README.md#version-compatibility).
|
||||
|
||||
### Deprecation
|
||||
|
||||
An API on its way out is marked with a `// Deprecated:` comment naming its
|
||||
replacement, which `gopls` and `staticcheck` surface at call sites. It keeps
|
||||
working until the next major version, since removing it would itself be a
|
||||
breaking change.
|
||||
|
||||
Protocol features that the *specification* deprecates are a separate matter and
|
||||
follow the spec's timeline rather than this policy. The features deprecated by
|
||||
[SEP-2577](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging)
|
||||
— roots, sampling and logging — remain supported for at least twelve months, as
|
||||
noted in the [README](README.md#version-compatibility).
|
||||
|
||||
Any change to the exported API, breaking or not, goes through the
|
||||
[proposal process](#proposals).
|
||||
|
||||
## Timeouts
|
||||
|
||||
If a contributor hasn't responded to issue questions or PR comments in two weeks,
|
||||
|
|
|
|||
212
docs/client.md
212
docs/client.md
|
|
@ -2,8 +2,11 @@
|
|||
# Support for MCP client features
|
||||
|
||||
1. [Roots](#roots)
|
||||
1. [Roots list changed](#roots-list-changed)
|
||||
1. [Sampling](#sampling)
|
||||
1. [Elicitation](#elicitation)
|
||||
1. [Schema defaults and enums](#schema-defaults-and-enums)
|
||||
1. [Completing a URL elicitation](#completing-a-url-elicitation)
|
||||
1. [Multi Round-Trip Requests](#multi-round-trip-requests)
|
||||
1. [Capabilities](#capabilities)
|
||||
1. [Capability inference](#capability-inference)
|
||||
|
|
@ -91,6 +94,67 @@ func Example_roots() {
|
|||
}
|
||||
```
|
||||
|
||||
### Roots list changed
|
||||
|
||||
`Client.AddRoots` and `Client.RemoveRoots` notify every connected server that
|
||||
the list changed. Servers observe this through
|
||||
[`ServerOptions.RootsListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.RootsListChangedHandler);
|
||||
as with the server-side list-changed notifications, it reports only that
|
||||
something changed, so read the list back with `ServerSession.ListRoots`.
|
||||
|
||||
```go
|
||||
func Example_rootsListChanged() {
|
||||
ctx := context.Background()
|
||||
|
||||
changed := make(chan struct{}, 2)
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
RootsListChangedHandler: func(context.Context, *mcp.RootsListChangedRequest) {
|
||||
changed <- struct{}{}
|
||||
},
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
c.AddRoots(&mcp.Root{URI: "file:///project"})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
ss, err := s.Connect(ctx, t1, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ss.Close()
|
||||
|
||||
// ListRoots is a server-initiated request, so this session negotiates a
|
||||
// protocol version that still allows one.
|
||||
cs, err := c.Connect(ctx, t2, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
// Roots added after the client connects notify every connected server.
|
||||
c.AddRoots(&mcp.Root{URI: "file:///scratch"})
|
||||
<-changed
|
||||
|
||||
// The notification says only that the list changed, so read it back.
|
||||
res, err := ss.ListRoots(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, root := range res.Roots {
|
||||
fmt.Println(root.URI)
|
||||
}
|
||||
|
||||
c.RemoveRoots("file:///scratch")
|
||||
<-changed
|
||||
fmt.Println("roots changed again")
|
||||
|
||||
// Output:
|
||||
// file:///project
|
||||
// file:///scratch
|
||||
// roots changed again
|
||||
}
|
||||
```
|
||||
|
||||
## Sampling
|
||||
|
||||
> **Note:** The sampling feature is deprecated as of protocol version
|
||||
|
|
@ -235,6 +299,154 @@ func Example_elicitation() {
|
|||
}
|
||||
```
|
||||
|
||||
### Schema defaults and enums
|
||||
|
||||
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
||||
client renders as a form. Two field keywords shape that form.
|
||||
|
||||
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034)) prefills a
|
||||
field. When the user accepts without supplying it, the SDK fills the field in
|
||||
from the schema before the result reaches either side's caller — the client
|
||||
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
||||
so again on receipt. This is unconditional; there is no opt-in flag. Marking a
|
||||
defaulted field `Required` defeats it: accepted content is validated against
|
||||
the schema before defaults are applied, so an answer that omits the field is
|
||||
rejected rather than defaulted.
|
||||
|
||||
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330)) restricts a
|
||||
field to a fixed set of values, which the client renders as a choice. Enums
|
||||
are supported only on `"string"` fields; declaring one on another type is
|
||||
rejected. To label the choices, set the legacy `enumNames` keyword through
|
||||
`Schema.Extra`, with exactly one name per enum value — a mismatched length is
|
||||
rejected.
|
||||
|
||||
```go
|
||||
func Example_elicitationSchema() {
|
||||
ctx := context.Background()
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(s, &mcp.Tool{Name: "export_report"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||
if len(req.Params.InputResponses) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{"format": &mcp.ElicitParams{
|
||||
Message: "Export quarterly-sales as which format?",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"format": {
|
||||
Type: "string",
|
||||
Title: "Format",
|
||||
Enum: []any{"pdf", "csv"},
|
||||
Default: json.RawMessage(`"pdf"`),
|
||||
Extra: map[string]any{"enumNames": []any{"PDF document", "CSV spreadsheet"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}, nil, nil
|
||||
}
|
||||
res := req.Params.InputResponses["format"].(*mcp.ElicitResult)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "Exported as " + res.Content["format"].(string)}},
|
||||
}, nil, nil
|
||||
})
|
||||
if _, err := s.Connect(ctx, st, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// The user accepts without filling anything in.
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
|
||||
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{}}, nil
|
||||
},
|
||||
})
|
||||
cs, err := c.Connect(ctx, ct, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "export_report"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
||||
// Output: Exported as pdf
|
||||
}
|
||||
```
|
||||
|
||||
### Completing a URL elicitation
|
||||
|
||||
In URL mode the user finishes out of band, in a browser, so nothing in the
|
||||
elicitation result tells the client when they are done. The server signals that
|
||||
with
|
||||
[`ServerSession.NotifyElicitationComplete`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.NotifyElicitationComplete),
|
||||
passing the same `ElicitationID` the request carried; the client observes it
|
||||
through
|
||||
[`ClientOptions.ElicitationCompleteHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationCompleteHandler).
|
||||
Send it from whatever endpoint the hosted flow redirects back to.
|
||||
|
||||
The notification matters most when a handler rejects a request with
|
||||
[`URLElicitationRequiredError`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#URLElicitationRequiredError):
|
||||
the client parks the original request until a notification names that
|
||||
`ElicitationID`, and then retries it automatically. Until one arrives, the
|
||||
client waits.
|
||||
|
||||
```go
|
||||
func Example_elicitationComplete() {
|
||||
ctx := context.Background()
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
ss, err := s.Connect(ctx, st, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ss.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
Capabilities: &mcp.ClientCapabilities{
|
||||
Elicitation: &mcp.ElicitationCapabilities{URL: &mcp.URLElicitationCapabilities{}},
|
||||
},
|
||||
ElicitationHandler: func(_ context.Context, req *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
|
||||
fmt.Println("opening", req.Params.URL)
|
||||
return &mcp.ElicitResult{Action: "accept"}, nil
|
||||
},
|
||||
ElicitationCompleteHandler: func(_ context.Context, req *mcp.ElicitationCompleteNotificationRequest) {
|
||||
fmt.Println("flow finished:", req.Params.ElicitationID)
|
||||
close(done)
|
||||
},
|
||||
})
|
||||
cs, err := c.Connect(ctx, ct, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
const elicitationID = "connect-calendar-1"
|
||||
if _, err := ss.Elicit(ctx, &mcp.ElicitParams{
|
||||
Message: "Grant calendar access",
|
||||
URL: "https://calendar.example.com/consent?state=" + elicitationID,
|
||||
ElicitationID: elicitationID,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// The hosted page redirects back to the server, whose callback endpoint
|
||||
// signals that the user is done.
|
||||
if err := ss.NotifyElicitationComplete(ctx, &mcp.ElicitationCompleteParams{ElicitationID: elicitationID}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
// Output:
|
||||
// opening https://calendar.example.com/consent?state=connect-calendar-1
|
||||
// flow finished: connect-calendar-1
|
||||
}
|
||||
```
|
||||
|
||||
## Multi Round-Trip Requests
|
||||
|
||||
[SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
1. [Transports](#transports)
|
||||
1. [Stdio Transport](#stdio-transport)
|
||||
1. [Streamable Transport](#streamable-transport)
|
||||
1. [SSE Transport (legacy)](#sse-transport-(legacy))
|
||||
1. [Custom transports](#custom-transports)
|
||||
1. [Concurrency](#concurrency)
|
||||
1. [Authorization](#authorization)
|
||||
|
|
@ -342,6 +343,63 @@ _See [examples/server/distributed](https://github.com/modelcontextprotocol/go-sd
|
|||
an example using stateless mode to implement a server distributed across
|
||||
multiple processes._
|
||||
|
||||
### SSE Transport (legacy)
|
||||
|
||||
Before the streamable transport, the
|
||||
[2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports)
|
||||
spec defined an HTTP transport built from two endpoints: a hanging GET that
|
||||
streams server-to-client messages as server-sent events, and a per-session
|
||||
endpoint the client POSTs its messages to. The SDK still implements it, for
|
||||
talking to peers that predate the streamable transport. New deployments should
|
||||
use the [streamable transport](#streamable-transport) instead.
|
||||
|
||||
It is implemented across three types, mirroring the streamable ones:
|
||||
|
||||
- [`SSEHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEHandler):
|
||||
an `http.Handler` that creates a session per incoming GET and routes POSTs to
|
||||
the right session. Construct it with
|
||||
[`NewSSEHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#NewSSEHandler),
|
||||
which takes a function returning the `Server` to serve a given request.
|
||||
- [`SSEServerTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEServerTransport):
|
||||
one such session. Reach for it directly only when serving the two endpoints
|
||||
yourself; `SSEHandler` creates these for you.
|
||||
- [`SSEClientTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEClientTransport):
|
||||
the client side, which needs only the `Endpoint` of the GET.
|
||||
|
||||
```go
|
||||
func ExampleSSEHandler() {
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "adder", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "add", Description: "add two numbers"}, Add)
|
||||
|
||||
handler := mcp.NewSSEHandler(func(*http.Request) *mcp.Server { return server }, nil)
|
||||
httpServer := httptest.NewServer(handler)
|
||||
defer httpServer.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
transport := &mcp.SSEClientTransport{Endpoint: httpServer.URL}
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "v1.0.0"}, nil)
|
||||
cs, err := client.Connect(ctx, transport, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.CallTool(ctx, &mcp.CallToolParams{
|
||||
Name: "add",
|
||||
Arguments: map[string]any{"x": 1, "y": 2},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
||||
|
||||
// Output: 3
|
||||
}
|
||||
```
|
||||
|
||||
_See [examples/server/sse](https://github.com/modelcontextprotocol/go-sdk/blob/main/examples/server/sse/main.go)
|
||||
for a standalone server._
|
||||
|
||||
### Custom transports
|
||||
|
||||
The SDK supports [custom
|
||||
|
|
|
|||
410
docs/server.md
410
docs/server.md
|
|
@ -2,8 +2,13 @@
|
|||
# Support for MCP server features
|
||||
|
||||
1. [Prompts](#prompts)
|
||||
1. [Prompt message content](#prompt-message-content)
|
||||
1. [Resources](#resources)
|
||||
1. [Binary resources](#binary-resources)
|
||||
1. [Resource subscriptions](#resource-subscriptions)
|
||||
1. [Tools](#tools)
|
||||
1. [Tool result content](#tool-result-content)
|
||||
1. [List changed notifications](#list-changed-notifications)
|
||||
1. [Multi Round-Trip Requests](#multi-round-trip-requests)
|
||||
1. [Talking to legacy clients](#talking-to-legacy-clients)
|
||||
1. [Example](#example)
|
||||
|
|
@ -115,6 +120,91 @@ func Example_prompts() {
|
|||
}
|
||||
```
|
||||
|
||||
### Prompt message content
|
||||
|
||||
A
|
||||
[`PromptMessage`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#PromptMessage)
|
||||
has two fields: a `Role`, either `"user"` or `"assistant"`, and a single
|
||||
`Content`. That is the same interface a tool result uses (see [Tool result
|
||||
content](#tool-result-content)), so a prompt message can hold an image, audio,
|
||||
or a resource's contents as readily as text.
|
||||
|
||||
Media on its own says nothing about what the model should do with it, so send a
|
||||
text message alongside it. Embedding a resource puts its contents directly in
|
||||
the message, sparing the client a separate `resources/read`. Note that
|
||||
`ResourceContents.URI` is not checked against the resources the server has
|
||||
registered: it records where the contents came from, so use the URI of a real
|
||||
resource when there is one, as in the example below.
|
||||
|
||||
```go
|
||||
func Example_promptContent() {
|
||||
ctx := context.Background()
|
||||
|
||||
const styleGuide = "- Prefer clarity over cleverness.\n"
|
||||
screenshotPNG := []byte("\x89PNG\r\n\x1a\n")
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
|
||||
s.AddResource(&mcp.Resource{URI: "doc://style-guide", MIMEType: "text/markdown"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{
|
||||
URI: "doc://style-guide",
|
||||
MIMEType: "text/markdown",
|
||||
Text: styleGuide,
|
||||
}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
s.AddPrompt(&mcp.Prompt{Name: "review_screenshot"},
|
||||
func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{Role: "user", Content: &mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "doc://style-guide",
|
||||
MIMEType: "text/markdown",
|
||||
Text: styleGuide,
|
||||
},
|
||||
}},
|
||||
{Role: "user", Content: &mcp.ImageContent{Data: screenshotPNG, MIMEType: "image/png"}},
|
||||
{Role: "user", Content: &mcp.TextContent{Text: "Review the screenshot against the style guide."}},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{Name: "review_screenshot"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, msg := range res.Messages {
|
||||
switch content := msg.Content.(type) {
|
||||
case *mcp.TextContent:
|
||||
fmt.Println(msg.Role, "text:", content.Text)
|
||||
case *mcp.ImageContent:
|
||||
fmt.Printf("%s image: %s, %d bytes\n", msg.Role, content.MIMEType, len(content.Data))
|
||||
case *mcp.EmbeddedResource:
|
||||
fmt.Printf("%s embedded resource: %s (%s)\n", msg.Role, content.Resource.URI, content.Resource.MIMEType)
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// user embedded resource: doc://style-guide (text/markdown)
|
||||
// user image: image/png, 8 bytes
|
||||
// user text: Review the screenshot against the style guide.
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
In MCP terms, a _resource_ is some data referenced by a URI.
|
||||
|
|
@ -246,6 +336,145 @@ func Example_resources() {
|
|||
}
|
||||
```
|
||||
|
||||
### Binary resources
|
||||
|
||||
A
|
||||
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents)
|
||||
carries its data in either `Text` or `Blob`. Binary data goes in `Blob` as
|
||||
plain bytes — the SDK base64-encodes it on the wire — and leaves `Text` empty,
|
||||
so a client tells the two apart by which field is populated.
|
||||
|
||||
```go
|
||||
func Example_binaryResource() {
|
||||
ctx := context.Background()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
s.AddResource(&mcp.Resource{URI: "file:///logo.png", MIMEType: "image/png"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{
|
||||
URI: "file:///logo.png",
|
||||
MIMEType: "image/png",
|
||||
Blob: []byte("\x89PNG\r\n\x1a\n"),
|
||||
}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "file:///logo.png"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
contents := res.Contents[0]
|
||||
fmt.Printf("%s: %d bytes of %s, text is %q\n",
|
||||
contents.URI, len(contents.Blob), contents.MIMEType, contents.Text)
|
||||
|
||||
// Output:
|
||||
// file:///logo.png: 8 bytes of image/png, text is ""
|
||||
}
|
||||
```
|
||||
|
||||
### Resource subscriptions
|
||||
|
||||
A client interested in changes to one resource subscribes to its URI with
|
||||
[`ClientSession.Subscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Subscribe)
|
||||
and stops with
|
||||
[`ClientSession.Unsubscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Unsubscribe).
|
||||
Set
|
||||
[`ServerOptions.SubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.SubscribeHandler)
|
||||
and
|
||||
[`UnsubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.UnsubscribeHandler)
|
||||
to track who is listening; setting either one gives the server the
|
||||
`resources.subscribe` capability.
|
||||
|
||||
Publish a change with
|
||||
[`Server.ResourceUpdated`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.ResourceUpdated),
|
||||
which notifies every session subscribed to that URI. The notification carries
|
||||
only the URI, not the new contents, so a client that wants them reads the
|
||||
resource again.
|
||||
|
||||
```go
|
||||
func Example_resourceSubscription() {
|
||||
ctx := context.Background()
|
||||
|
||||
config := "theme=light\n"
|
||||
|
||||
updated := make(chan string, 1)
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
// The notification carries only the URI, so read the resource again to
|
||||
// see what it now holds.
|
||||
ResourceUpdatedHandler: func(context.Context, *mcp.ResourceUpdatedNotificationRequest) {
|
||||
updated <- "config://app"
|
||||
},
|
||||
})
|
||||
|
||||
subscribed := make(chan string, 2)
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
SubscribeHandler: func(_ context.Context, req *mcp.SubscribeRequest) error {
|
||||
subscribed <- "subscribed to " + req.Params.URI
|
||||
return nil
|
||||
},
|
||||
UnsubscribeHandler: func(_ context.Context, req *mcp.UnsubscribeRequest) error {
|
||||
subscribed <- "unsubscribed from " + req.Params.URI
|
||||
return nil
|
||||
},
|
||||
})
|
||||
s.AddResource(&mcp.Resource{URI: "config://app"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{URI: "config://app", Text: config}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
if err := cs.Subscribe(ctx, &mcp.SubscribeParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(<-subscribed)
|
||||
|
||||
config = "theme=dark\n"
|
||||
if err := s.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
uri := <-updated
|
||||
|
||||
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: uri})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Print("re-read: ", res.Contents[0].Text)
|
||||
|
||||
if err := cs.Unsubscribe(ctx, &mcp.UnsubscribeParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(<-subscribed)
|
||||
|
||||
// Output:
|
||||
// subscribed to config://app
|
||||
// re-read: theme=dark
|
||||
// unsubscribed from config://app
|
||||
}
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
MCP servers can provide
|
||||
|
|
@ -297,7 +526,7 @@ In order to implement a tool, the user must do all of the following:
|
|||
- Unmarshal the input schema into a Go value
|
||||
- Execute the tool logic.
|
||||
- Marshal the tool's structured output (if any) to JSON, and store it in the
|
||||
result's `StructuredOutput` field as well as the unstructured `Content` field.
|
||||
result's `StructuredContent` field as well as the unstructured `Content` field.
|
||||
- Validate that output JSON against the tool's output schema.
|
||||
- If any tool errors occurred, pack them into the unstructured content and set
|
||||
`IsError` to `true.`
|
||||
|
|
@ -332,7 +561,7 @@ This does the following automatically:
|
|||
- Tool arguments are validated against the input schema.
|
||||
- Tool arguments are marshaled into the `In` value.
|
||||
- Tool output (the `Out` value) is marshaled into the result's
|
||||
`StructuredOutput`, as well as the unstructured `Content`.
|
||||
`StructuredContent`, as well as the unstructured `Content`.
|
||||
- Output is validated against the tool's output schema.
|
||||
- If an ordinary error is returned, it is stored int the `CallToolResult` and
|
||||
`IsError` is set to `true`.
|
||||
|
|
@ -417,6 +646,103 @@ _See [mcp/tool_example_test.go](https://github.com/modelcontextprotocol/go-sdk/b
|
|||
example, or [examples/server/toolschemas](https://github.com/modelcontextprotocol/go-sdk/blob/main/examples/server/toolschemas/main.go)
|
||||
for more examples of customizing tool schemas._
|
||||
|
||||
### Tool result content
|
||||
|
||||
Alongside its structured output, a tool result carries a list of
|
||||
[`Content`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Content)
|
||||
blocks in `CallToolResult.Content`, and may mix as many kinds as it needs:
|
||||
|
||||
| Type | Carries |
|
||||
| --- | --- |
|
||||
| [`TextContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#TextContent) | plain text |
|
||||
| [`ImageContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ImageContent) | image data, with a MIME type |
|
||||
| [`AudioContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AudioContent) | audio data, with a MIME type |
|
||||
| [`EmbeddedResource`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#EmbeddedResource) | the contents of a resource, inline |
|
||||
| [`ResourceLink`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceLink) | a reference to a resource the client can read separately |
|
||||
|
||||
`ImageContent.Data` and `AudioContent.Data` are plain `[]byte`; the SDK
|
||||
base64-encodes them on the wire, so handlers never encode by hand, and base64
|
||||
carried over from another MCP SDK must be decoded before it is assigned. An
|
||||
`EmbeddedResource` wraps a
|
||||
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents),
|
||||
which holds either `Text` or, for binary data, `Blob`. Prefer a `ResourceLink`
|
||||
when the client may not need the contents, since an embedded resource is
|
||||
transferred whether it is used or not.
|
||||
|
||||
When a handler bound with `AddTool` leaves `Content` unset, the SDK fills it
|
||||
with the JSON encoding of the output value; set `Content` explicitly, as below,
|
||||
to return anything else. `StructuredContent` is still populated from the output
|
||||
value either way.
|
||||
|
||||
```go
|
||||
func ExampleAddTool_contentTypes() {
|
||||
chartPNG := []byte("\x89PNG\r\n\x1a\n")
|
||||
summaryWAV := []byte("RIFF....WAVE")
|
||||
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "report"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Q3 revenue by region."},
|
||||
&mcp.ImageContent{Data: chartPNG, MIMEType: "image/png"},
|
||||
&mcp.AudioContent{Data: summaryWAV, MIMEType: "audio/wav"},
|
||||
// An embedded resource inlines the contents, so the client
|
||||
// needs no follow-up resources/read. Binary contents go in
|
||||
// ResourceContents.Blob instead of Text.
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "file:///reports/q3.csv",
|
||||
MIMEType: "text/csv",
|
||||
Text: "region,revenue\nEMEA,42\n",
|
||||
},
|
||||
},
|
||||
// A resource link only points at a resource. Prefer it when the
|
||||
// client may not need the contents, since an embedded resource
|
||||
// is transferred either way.
|
||||
&mcp.ResourceLink{URI: "file:///reports/q3.pdf", MIMEType: "application/pdf"},
|
||||
},
|
||||
}, nil, nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
session, err := connect(ctx, server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "report"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Clients type-switch over the content they receive.
|
||||
for _, content := range res.Content {
|
||||
switch c := content.(type) {
|
||||
case *mcp.TextContent:
|
||||
fmt.Println("text:", c.Text)
|
||||
case *mcp.ImageContent:
|
||||
fmt.Printf("image: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
||||
case *mcp.AudioContent:
|
||||
fmt.Printf("audio: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
||||
case *mcp.EmbeddedResource:
|
||||
fmt.Printf("embedded resource: %s (%s)\n", c.Resource.URI, c.Resource.MIMEType)
|
||||
case *mcp.ResourceLink:
|
||||
fmt.Printf("resource link: %s (%s)\n", c.URI, c.MIMEType)
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// text: Q3 revenue by region.
|
||||
// image: image/png, 8 bytes
|
||||
// audio: audio/wav, 12 bytes
|
||||
// embedded resource: file:///reports/q3.csv (text/csv)
|
||||
// resource link: file:///reports/q3.pdf (application/pdf)
|
||||
}
|
||||
```
|
||||
|
||||
`ToolUseContent` and `ToolResultContent` also implement `Content`, but are only
|
||||
valid in sampling messages, not in tool results.
|
||||
|
||||
**Stateless server deployments:** Some deployments create a new
|
||||
[`Server`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server)
|
||||
for each incoming request, re-registering tools every time. To avoid repeated
|
||||
|
|
@ -434,6 +760,86 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
```
|
||||
|
||||
## List changed notifications
|
||||
|
||||
Adding or removing a feature on a connected server sends the matching
|
||||
`notifications/*/list_changed` to every client, which dispatches it to
|
||||
[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler),
|
||||
[`PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler),
|
||||
or
|
||||
[`ResourceListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ResourceListChangedHandler).
|
||||
A notification reports only that the list changed, so a client that needs the
|
||||
new contents lists them again.
|
||||
|
||||
These notifications ride on capabilities, and capabilities are inferred from
|
||||
what is registered before [`Server.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.Connect)
|
||||
(see [Capabilities](#capabilities)). A server that registers its features
|
||||
afterwards, as the example below does, has to declare
|
||||
`ServerOptions.HasTools`, `HasPrompts`, or `HasResources` itself, or the
|
||||
client is never told.
|
||||
|
||||
```go
|
||||
func Example_listChanged() {
|
||||
ctx := context.Background()
|
||||
|
||||
changed := make(chan string, 3)
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
|
||||
changed <- "tools"
|
||||
},
|
||||
PromptListChangedHandler: func(context.Context, *mcp.PromptListChangedRequest) {
|
||||
changed <- "prompts"
|
||||
},
|
||||
ResourceListChangedHandler: func(context.Context, *mcp.ResourceListChangedRequest) {
|
||||
changed <- "resources"
|
||||
},
|
||||
})
|
||||
|
||||
// Nothing is registered before Connect, so the capabilities that carry these
|
||||
// notifications have to be declared explicitly.
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
HasTools: true,
|
||||
HasPrompts: true,
|
||||
HasResources: true,
|
||||
})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
mcp.AddTool(s, &mcp.Tool{Name: "greet"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{}, nil, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
s.AddPrompt(&mcp.Prompt{Name: "review"}, func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{}, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
s.AddResource(&mcp.Resource{URI: "file:///a"}, func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{}, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
// Removing a feature notifies the client just as adding one does.
|
||||
s.RemoveTools("greet")
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
// Output:
|
||||
// changed: tools
|
||||
// changed: prompts
|
||||
// changed: resources
|
||||
// changed: tools
|
||||
}
|
||||
```
|
||||
|
||||
## Multi Round-Trip Requests
|
||||
|
||||
[SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ pattern.
|
|||
|
||||
%include ../../mcp/client_example_test.go roots -
|
||||
|
||||
### Roots list changed
|
||||
|
||||
`Client.AddRoots` and `Client.RemoveRoots` notify every connected server that
|
||||
the list changed. Servers observe this through
|
||||
[`ServerOptions.RootsListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.RootsListChangedHandler);
|
||||
as with the server-side list-changed notifications, it reports only that
|
||||
something changed, so read the list back with `ServerSession.ListRoots`.
|
||||
|
||||
%include ../../mcp/client_example_test.go rootslistchanged -
|
||||
|
||||
## Sampling
|
||||
|
||||
> **Note:** The sampling feature is deprecated as of protocol version
|
||||
|
|
@ -84,6 +94,48 @@ pattern.
|
|||
|
||||
%include ../../mcp/client_example_test.go elicitation -
|
||||
|
||||
### Schema defaults and enums
|
||||
|
||||
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
||||
client renders as a form. Two field keywords shape that form.
|
||||
|
||||
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034)) prefills a
|
||||
field. When the user accepts without supplying it, the SDK fills the field in
|
||||
from the schema before the result reaches either side's caller — the client
|
||||
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
||||
so again on receipt. This is unconditional; there is no opt-in flag. Marking a
|
||||
defaulted field `Required` defeats it: accepted content is validated against
|
||||
the schema before defaults are applied, so an answer that omits the field is
|
||||
rejected rather than defaulted.
|
||||
|
||||
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330)) restricts a
|
||||
field to a fixed set of values, which the client renders as a choice. Enums
|
||||
are supported only on `"string"` fields; declaring one on another type is
|
||||
rejected. To label the choices, set the legacy `enumNames` keyword through
|
||||
`Schema.Extra`, with exactly one name per enum value — a mismatched length is
|
||||
rejected.
|
||||
|
||||
%include ../../mcp/client_example_test.go elicitationschema -
|
||||
|
||||
### Completing a URL elicitation
|
||||
|
||||
In URL mode the user finishes out of band, in a browser, so nothing in the
|
||||
elicitation result tells the client when they are done. The server signals that
|
||||
with
|
||||
[`ServerSession.NotifyElicitationComplete`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.NotifyElicitationComplete),
|
||||
passing the same `ElicitationID` the request carried; the client observes it
|
||||
through
|
||||
[`ClientOptions.ElicitationCompleteHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationCompleteHandler).
|
||||
Send it from whatever endpoint the hosted flow redirects back to.
|
||||
|
||||
The notification matters most when a handler rejects a request with
|
||||
[`URLElicitationRequiredError`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#URLElicitationRequiredError):
|
||||
the client parks the original request until a notification names that
|
||||
`ElicitationID`, and then retries it automatically. Until one arrives, the
|
||||
client waits.
|
||||
|
||||
%include ../../mcp/client_example_test.go elicitationcomplete -
|
||||
|
||||
## Multi Round-Trip Requests
|
||||
|
||||
[SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)
|
||||
|
|
|
|||
|
|
@ -267,6 +267,34 @@ _See [examples/server/distributed](https://github.com/modelcontextprotocol/go-sd
|
|||
an example using stateless mode to implement a server distributed across
|
||||
multiple processes._
|
||||
|
||||
### SSE Transport (legacy)
|
||||
|
||||
Before the streamable transport, the
|
||||
[2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports)
|
||||
spec defined an HTTP transport built from two endpoints: a hanging GET that
|
||||
streams server-to-client messages as server-sent events, and a per-session
|
||||
endpoint the client POSTs its messages to. The SDK still implements it, for
|
||||
talking to peers that predate the streamable transport. New deployments should
|
||||
use the [streamable transport](#streamable-transport) instead.
|
||||
|
||||
It is implemented across three types, mirroring the streamable ones:
|
||||
|
||||
- [`SSEHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEHandler):
|
||||
an `http.Handler` that creates a session per incoming GET and routes POSTs to
|
||||
the right session. Construct it with
|
||||
[`NewSSEHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#NewSSEHandler),
|
||||
which takes a function returning the `Server` to serve a given request.
|
||||
- [`SSEServerTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEServerTransport):
|
||||
one such session. Reach for it directly only when serving the two endpoints
|
||||
yourself; `SSEHandler` creates these for you.
|
||||
- [`SSEClientTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SSEClientTransport):
|
||||
the client side, which needs only the `Endpoint` of the GET.
|
||||
|
||||
%include ../../mcp/sse_example_test.go ssehandler -
|
||||
|
||||
_See [examples/server/sse](https://github.com/modelcontextprotocol/go-sdk/blob/main/examples/server/sse/main.go)
|
||||
for a standalone server._
|
||||
|
||||
### Custom transports
|
||||
|
||||
The SDK supports [custom
|
||||
|
|
|
|||
|
|
@ -33,6 +33,24 @@ notification.
|
|||
|
||||
%include ../../mcp/server_example_test.go prompts -
|
||||
|
||||
### Prompt message content
|
||||
|
||||
A
|
||||
[`PromptMessage`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#PromptMessage)
|
||||
has two fields: a `Role`, either `"user"` or `"assistant"`, and a single
|
||||
`Content`. That is the same interface a tool result uses (see [Tool result
|
||||
content](#tool-result-content)), so a prompt message can hold an image, audio,
|
||||
or a resource's contents as readily as text.
|
||||
|
||||
Media on its own says nothing about what the model should do with it, so send a
|
||||
text message alongside it. Embedding a resource puts its contents directly in
|
||||
the message, sparing the client a separate `resources/read`. Note that
|
||||
`ResourceContents.URI` is not checked against the resources the server has
|
||||
registered: it records where the contents came from, so use the URI of a real
|
||||
resource when there is one, as in the example below.
|
||||
|
||||
%include ../../mcp/server_example_test.go promptcontent -
|
||||
|
||||
## Resources
|
||||
|
||||
In MCP terms, a _resource_ is some data referenced by a URI.
|
||||
|
|
@ -94,6 +112,37 @@ notification.
|
|||
|
||||
%include ../../mcp/server_example_test.go resources -
|
||||
|
||||
### Binary resources
|
||||
|
||||
A
|
||||
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents)
|
||||
carries its data in either `Text` or `Blob`. Binary data goes in `Blob` as
|
||||
plain bytes — the SDK base64-encodes it on the wire — and leaves `Text` empty,
|
||||
so a client tells the two apart by which field is populated.
|
||||
|
||||
%include ../../mcp/server_example_test.go binaryresource -
|
||||
|
||||
### Resource subscriptions
|
||||
|
||||
A client interested in changes to one resource subscribes to its URI with
|
||||
[`ClientSession.Subscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Subscribe)
|
||||
and stops with
|
||||
[`ClientSession.Unsubscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Unsubscribe).
|
||||
Set
|
||||
[`ServerOptions.SubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.SubscribeHandler)
|
||||
and
|
||||
[`UnsubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.UnsubscribeHandler)
|
||||
to track who is listening; setting either one gives the server the
|
||||
`resources.subscribe` capability.
|
||||
|
||||
Publish a change with
|
||||
[`Server.ResourceUpdated`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.ResourceUpdated),
|
||||
which notifies every session subscribed to that URI. The notification carries
|
||||
only the URI, not the new contents, so a client that wants them reads the
|
||||
resource again.
|
||||
|
||||
%include ../../mcp/server_example_test.go subscribe -
|
||||
|
||||
## Tools
|
||||
|
||||
MCP servers can provide
|
||||
|
|
@ -145,7 +194,7 @@ In order to implement a tool, the user must do all of the following:
|
|||
- Unmarshal the input schema into a Go value
|
||||
- Execute the tool logic.
|
||||
- Marshal the tool's structured output (if any) to JSON, and store it in the
|
||||
result's `StructuredOutput` field as well as the unstructured `Content` field.
|
||||
result's `StructuredContent` field as well as the unstructured `Content` field.
|
||||
- Validate that output JSON against the tool's output schema.
|
||||
- If any tool errors occurred, pack them into the unstructured content and set
|
||||
`IsError` to `true.`
|
||||
|
|
@ -180,7 +229,7 @@ This does the following automatically:
|
|||
- Tool arguments are validated against the input schema.
|
||||
- Tool arguments are marshaled into the `In` value.
|
||||
- Tool output (the `Out` value) is marshaled into the result's
|
||||
`StructuredOutput`, as well as the unstructured `Content`.
|
||||
`StructuredContent`, as well as the unstructured `Content`.
|
||||
- Output is validated against the tool's output schema.
|
||||
- If an ordinary error is returned, it is stored int the `CallToolResult` and
|
||||
`IsError` is set to `true`.
|
||||
|
|
@ -202,6 +251,39 @@ _See [mcp/tool_example_test.go](https://github.com/modelcontextprotocol/go-sdk/b
|
|||
example, or [examples/server/toolschemas](https://github.com/modelcontextprotocol/go-sdk/blob/main/examples/server/toolschemas/main.go)
|
||||
for more examples of customizing tool schemas._
|
||||
|
||||
### Tool result content
|
||||
|
||||
Alongside its structured output, a tool result carries a list of
|
||||
[`Content`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Content)
|
||||
blocks in `CallToolResult.Content`, and may mix as many kinds as it needs:
|
||||
|
||||
| Type | Carries |
|
||||
| --- | --- |
|
||||
| [`TextContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#TextContent) | plain text |
|
||||
| [`ImageContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ImageContent) | image data, with a MIME type |
|
||||
| [`AudioContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AudioContent) | audio data, with a MIME type |
|
||||
| [`EmbeddedResource`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#EmbeddedResource) | the contents of a resource, inline |
|
||||
| [`ResourceLink`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceLink) | a reference to a resource the client can read separately |
|
||||
|
||||
`ImageContent.Data` and `AudioContent.Data` are plain `[]byte`; the SDK
|
||||
base64-encodes them on the wire, so handlers never encode by hand, and base64
|
||||
carried over from another MCP SDK must be decoded before it is assigned. An
|
||||
`EmbeddedResource` wraps a
|
||||
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents),
|
||||
which holds either `Text` or, for binary data, `Blob`. Prefer a `ResourceLink`
|
||||
when the client may not need the contents, since an embedded resource is
|
||||
transferred whether it is used or not.
|
||||
|
||||
When a handler bound with `AddTool` leaves `Content` unset, the SDK fills it
|
||||
with the JSON encoding of the output value; set `Content` explicitly, as below,
|
||||
to return anything else. `StructuredContent` is still populated from the output
|
||||
value either way.
|
||||
|
||||
%include ../../mcp/tool_example_test.go contenttypes -
|
||||
|
||||
`ToolUseContent` and `ToolResultContent` also implement `Content`, but are only
|
||||
valid in sampling messages, not in tool results.
|
||||
|
||||
**Stateless server deployments:** Some deployments create a new
|
||||
[`Server`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server)
|
||||
for each incoming request, re-registering tools every time. To avoid repeated
|
||||
|
|
@ -219,6 +301,26 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
```
|
||||
|
||||
## List changed notifications
|
||||
|
||||
Adding or removing a feature on a connected server sends the matching
|
||||
`notifications/*/list_changed` to every client, which dispatches it to
|
||||
[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler),
|
||||
[`PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler),
|
||||
or
|
||||
[`ResourceListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ResourceListChangedHandler).
|
||||
A notification reports only that the list changed, so a client that needs the
|
||||
new contents lists them again.
|
||||
|
||||
These notifications ride on capabilities, and capabilities are inferred from
|
||||
what is registered before [`Server.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.Connect)
|
||||
(see [Capabilities](#capabilities)). A server that registers its features
|
||||
afterwards, as the example below does, has to declare
|
||||
`ServerOptions.HasTools`, `HasPrompts`, or `HasResources` itself, or the
|
||||
client is never told.
|
||||
|
||||
%include ../../mcp/server_example_test.go listchanged -
|
||||
|
||||
## Multi Round-Trip Requests
|
||||
|
||||
[SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)
|
||||
|
|
|
|||
|
|
@ -173,6 +173,57 @@ The CI system will automatically check that the README is up-to-date by running
|
|||
`go generate ./internal/readme` and verifying no changes result. If you see a CI failure about the
|
||||
README being out of sync, follow the steps above to regenerate it.
|
||||
|
||||
## Versioning
|
||||
|
||||
The SDK follows [semantic versioning](https://semver.org), as the Go module
|
||||
system requires: the major version is part of the module path, so a breaking
|
||||
release would import as `github.com/modelcontextprotocol/go-sdk/v2`. A breaking
|
||||
change therefore cannot reach existing users by accident; they have to change
|
||||
their import path to receive one.
|
||||
|
||||
This policy covers the exported API of the SDK's importable packages — `mcp`,
|
||||
`jsonrpc`, `auth`, `auth/extauth` and `oauthex`. Everything under `internal/`
|
||||
is not importable outside the module and may change in any release.
|
||||
|
||||
Which MCP spec revisions each SDK version speaks is documented in the
|
||||
[README](README.md#version-compatibility). Support for a new revision arrives
|
||||
in a minor release; dropping a revision the SDK previously negotiated is a
|
||||
breaking change.
|
||||
|
||||
### What counts as a breaking change
|
||||
|
||||
- Removing or renaming an exported identifier.
|
||||
- Changing the signature of an exported function or method.
|
||||
- Removing or renaming a field of an exported struct, or changing its type.
|
||||
- Adding a method to an exported interface that users are meant to implement.
|
||||
- Changing documented behavior that callers could reasonably rely on.
|
||||
- Dropping support for an MCP spec revision.
|
||||
|
||||
The following are not breaking, and may land in a minor or patch release:
|
||||
|
||||
- Adding an exported identifier, or a field to an exported struct.
|
||||
- Fixing behavior that contradicted its documentation.
|
||||
- Refactoring that leaves the exported API unchanged.
|
||||
- Adding support for a new MCP spec revision.
|
||||
- Raising the minimum Go version to one still supported upstream, per the
|
||||
[README](README.md#version-compatibility).
|
||||
|
||||
### Deprecation
|
||||
|
||||
An API on its way out is marked with a `// Deprecated:` comment naming its
|
||||
replacement, which `gopls` and `staticcheck` surface at call sites. It keeps
|
||||
working until the next major version, since removing it would itself be a
|
||||
breaking change.
|
||||
|
||||
Protocol features that the *specification* deprecates are a separate matter and
|
||||
follow the spec's timeline rather than this policy. The features deprecated by
|
||||
[SEP-2577](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging)
|
||||
— roots, sampling and logging — remain supported for at least twelve months, as
|
||||
noted in the [README](README.md#version-compatibility).
|
||||
|
||||
Any change to the exported API, breaking or not, goes through the
|
||||
[proposal process](#proposals).
|
||||
|
||||
## Timeouts
|
||||
|
||||
If a contributor hasn't responded to issue questions or PR comments in two weeks,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package mcp_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
|
|
@ -65,6 +66,61 @@ func Example_roots() {
|
|||
|
||||
// !-roots
|
||||
|
||||
// !+rootslistchanged
|
||||
|
||||
func Example_rootsListChanged() {
|
||||
ctx := context.Background()
|
||||
|
||||
changed := make(chan struct{}, 2)
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
RootsListChangedHandler: func(context.Context, *mcp.RootsListChangedRequest) {
|
||||
changed <- struct{}{}
|
||||
},
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
c.AddRoots(&mcp.Root{URI: "file:///project"})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
ss, err := s.Connect(ctx, t1, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ss.Close()
|
||||
|
||||
// ListRoots is a server-initiated request, so this session negotiates a
|
||||
// protocol version that still allows one.
|
||||
cs, err := c.Connect(ctx, t2, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
// Roots added after the client connects notify every connected server.
|
||||
c.AddRoots(&mcp.Root{URI: "file:///scratch"})
|
||||
<-changed
|
||||
|
||||
// The notification says only that the list changed, so read it back.
|
||||
res, err := ss.ListRoots(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, root := range res.Roots {
|
||||
fmt.Println(root.URI)
|
||||
}
|
||||
|
||||
c.RemoveRoots("file:///scratch")
|
||||
<-changed
|
||||
fmt.Println("roots changed again")
|
||||
|
||||
// Output:
|
||||
// file:///project
|
||||
// file:///scratch
|
||||
// roots changed again
|
||||
}
|
||||
|
||||
// !-rootslistchanged
|
||||
|
||||
// !+sampling
|
||||
|
||||
func Example_sampling() {
|
||||
|
|
@ -167,3 +223,117 @@ func Example_elicitation() {
|
|||
}
|
||||
|
||||
// !-elicitation
|
||||
|
||||
// !+elicitationschema
|
||||
|
||||
func Example_elicitationSchema() {
|
||||
ctx := context.Background()
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(s, &mcp.Tool{Name: "export_report"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||
if len(req.Params.InputResponses) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{"format": &mcp.ElicitParams{
|
||||
Message: "Export quarterly-sales as which format?",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"format": {
|
||||
Type: "string",
|
||||
Title: "Format",
|
||||
Enum: []any{"pdf", "csv"},
|
||||
Default: json.RawMessage(`"pdf"`),
|
||||
Extra: map[string]any{"enumNames": []any{"PDF document", "CSV spreadsheet"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}, nil, nil
|
||||
}
|
||||
res := req.Params.InputResponses["format"].(*mcp.ElicitResult)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "Exported as " + res.Content["format"].(string)}},
|
||||
}, nil, nil
|
||||
})
|
||||
if _, err := s.Connect(ctx, st, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// The user accepts without filling anything in.
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
|
||||
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{}}, nil
|
||||
},
|
||||
})
|
||||
cs, err := c.Connect(ctx, ct, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "export_report"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
||||
// Output: Exported as pdf
|
||||
}
|
||||
|
||||
// !-elicitationschema
|
||||
|
||||
// !+elicitationcomplete
|
||||
|
||||
func Example_elicitationComplete() {
|
||||
ctx := context.Background()
|
||||
ct, st := mcp.NewInMemoryTransports()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
ss, err := s.Connect(ctx, st, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ss.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
Capabilities: &mcp.ClientCapabilities{
|
||||
Elicitation: &mcp.ElicitationCapabilities{URL: &mcp.URLElicitationCapabilities{}},
|
||||
},
|
||||
ElicitationHandler: func(_ context.Context, req *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
|
||||
fmt.Println("opening", req.Params.URL)
|
||||
return &mcp.ElicitResult{Action: "accept"}, nil
|
||||
},
|
||||
ElicitationCompleteHandler: func(_ context.Context, req *mcp.ElicitationCompleteNotificationRequest) {
|
||||
fmt.Println("flow finished:", req.Params.ElicitationID)
|
||||
close(done)
|
||||
},
|
||||
})
|
||||
cs, err := c.Connect(ctx, ct, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
const elicitationID = "connect-calendar-1"
|
||||
if _, err := ss.Elicit(ctx, &mcp.ElicitParams{
|
||||
Message: "Grant calendar access",
|
||||
URL: "https://calendar.example.com/consent?state=" + elicitationID,
|
||||
ElicitationID: elicitationID,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// The hosted page redirects back to the server, whose callback endpoint
|
||||
// signals that the user is done.
|
||||
if err := ss.NotifyElicitationComplete(ctx, &mcp.ElicitationCompleteParams{ElicitationID: elicitationID}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
// Output:
|
||||
// opening https://calendar.example.com/consent?state=connect-calendar-1
|
||||
// flow finished: connect-calendar-1
|
||||
}
|
||||
|
||||
// !-elicitationcomplete
|
||||
|
|
|
|||
|
|
@ -88,6 +88,77 @@ func Example_prompts() {
|
|||
|
||||
// !-prompts
|
||||
|
||||
// !+promptcontent
|
||||
|
||||
func Example_promptContent() {
|
||||
ctx := context.Background()
|
||||
|
||||
const styleGuide = "- Prefer clarity over cleverness.\n"
|
||||
screenshotPNG := []byte("\x89PNG\r\n\x1a\n")
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
|
||||
s.AddResource(&mcp.Resource{URI: "doc://style-guide", MIMEType: "text/markdown"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{
|
||||
URI: "doc://style-guide",
|
||||
MIMEType: "text/markdown",
|
||||
Text: styleGuide,
|
||||
}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
s.AddPrompt(&mcp.Prompt{Name: "review_screenshot"},
|
||||
func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{Role: "user", Content: &mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "doc://style-guide",
|
||||
MIMEType: "text/markdown",
|
||||
Text: styleGuide,
|
||||
},
|
||||
}},
|
||||
{Role: "user", Content: &mcp.ImageContent{Data: screenshotPNG, MIMEType: "image/png"}},
|
||||
{Role: "user", Content: &mcp.TextContent{Text: "Review the screenshot against the style guide."}},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{Name: "review_screenshot"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, msg := range res.Messages {
|
||||
switch content := msg.Content.(type) {
|
||||
case *mcp.TextContent:
|
||||
fmt.Println(msg.Role, "text:", content.Text)
|
||||
case *mcp.ImageContent:
|
||||
fmt.Printf("%s image: %s, %d bytes\n", msg.Role, content.MIMEType, len(content.Data))
|
||||
case *mcp.EmbeddedResource:
|
||||
fmt.Printf("%s embedded resource: %s (%s)\n", msg.Role, content.Resource.URI, content.Resource.MIMEType)
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// user embedded resource: doc://style-guide (text/markdown)
|
||||
// user image: image/png, 8 bytes
|
||||
// user text: Review the screenshot against the style guide.
|
||||
}
|
||||
|
||||
// !-promptcontent
|
||||
|
||||
// !+logging
|
||||
|
||||
func Example_logging() {
|
||||
|
|
@ -222,6 +293,186 @@ func Example_resources() {
|
|||
|
||||
// !-resources
|
||||
|
||||
// !+listchanged
|
||||
|
||||
func Example_listChanged() {
|
||||
ctx := context.Background()
|
||||
|
||||
changed := make(chan string, 3)
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
|
||||
changed <- "tools"
|
||||
},
|
||||
PromptListChangedHandler: func(context.Context, *mcp.PromptListChangedRequest) {
|
||||
changed <- "prompts"
|
||||
},
|
||||
ResourceListChangedHandler: func(context.Context, *mcp.ResourceListChangedRequest) {
|
||||
changed <- "resources"
|
||||
},
|
||||
})
|
||||
|
||||
// Nothing is registered before Connect, so the capabilities that carry these
|
||||
// notifications have to be declared explicitly.
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
HasTools: true,
|
||||
HasPrompts: true,
|
||||
HasResources: true,
|
||||
})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
mcp.AddTool(s, &mcp.Tool{Name: "greet"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{}, nil, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
s.AddPrompt(&mcp.Prompt{Name: "review"}, func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{}, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
s.AddResource(&mcp.Resource{URI: "file:///a"}, func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{}, nil
|
||||
})
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
// Removing a feature notifies the client just as adding one does.
|
||||
s.RemoveTools("greet")
|
||||
fmt.Println("changed:", <-changed)
|
||||
|
||||
// Output:
|
||||
// changed: tools
|
||||
// changed: prompts
|
||||
// changed: resources
|
||||
// changed: tools
|
||||
}
|
||||
|
||||
// !-listchanged
|
||||
|
||||
// !+subscribe
|
||||
|
||||
func Example_resourceSubscription() {
|
||||
ctx := context.Background()
|
||||
|
||||
config := "theme=light\n"
|
||||
|
||||
updated := make(chan string, 1)
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
|
||||
// The notification carries only the URI, so read the resource again to
|
||||
// see what it now holds.
|
||||
ResourceUpdatedHandler: func(context.Context, *mcp.ResourceUpdatedNotificationRequest) {
|
||||
updated <- "config://app"
|
||||
},
|
||||
})
|
||||
|
||||
subscribed := make(chan string, 2)
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
||||
SubscribeHandler: func(_ context.Context, req *mcp.SubscribeRequest) error {
|
||||
subscribed <- "subscribed to " + req.Params.URI
|
||||
return nil
|
||||
},
|
||||
UnsubscribeHandler: func(_ context.Context, req *mcp.UnsubscribeRequest) error {
|
||||
subscribed <- "unsubscribed from " + req.Params.URI
|
||||
return nil
|
||||
},
|
||||
})
|
||||
s.AddResource(&mcp.Resource{URI: "config://app"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{URI: "config://app", Text: config}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
if err := cs.Subscribe(ctx, &mcp.SubscribeParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(<-subscribed)
|
||||
|
||||
config = "theme=dark\n"
|
||||
if err := s.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
uri := <-updated
|
||||
|
||||
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: uri})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Print("re-read: ", res.Contents[0].Text)
|
||||
|
||||
if err := cs.Unsubscribe(ctx, &mcp.UnsubscribeParams{URI: "config://app"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(<-subscribed)
|
||||
|
||||
// Output:
|
||||
// subscribed to config://app
|
||||
// re-read: theme=dark
|
||||
// unsubscribed from config://app
|
||||
}
|
||||
|
||||
// !-subscribe
|
||||
|
||||
// !+binaryresource
|
||||
|
||||
func Example_binaryResource() {
|
||||
ctx := context.Background()
|
||||
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
s.AddResource(&mcp.Resource{URI: "file:///logo.png", MIMEType: "image/png"},
|
||||
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{{
|
||||
URI: "file:///logo.png",
|
||||
MIMEType: "image/png",
|
||||
Blob: []byte("\x89PNG\r\n\x1a\n"),
|
||||
}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := s.Connect(ctx, t1, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cs, err := c.Connect(ctx, t2, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cs.Close()
|
||||
|
||||
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "file:///logo.png"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
contents := res.Contents[0]
|
||||
fmt.Printf("%s: %d bytes of %s, text is %q\n",
|
||||
contents.URI, len(contents.Blob), contents.MIMEType, contents.Text)
|
||||
|
||||
// Output:
|
||||
// file:///logo.png: 8 bytes of image/png, text is ""
|
||||
}
|
||||
|
||||
// !-binaryresource
|
||||
|
||||
// !+mrtr
|
||||
|
||||
// Example_mrtr demonstrates the [Multi Round-Trip Requests] pattern
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ func Add(ctx context.Context, req *mcp.CallToolRequest, args AddParams) (*mcp.Ca
|
|||
}, nil, nil
|
||||
}
|
||||
|
||||
// !+ssehandler
|
||||
|
||||
func ExampleSSEHandler() {
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "adder", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "add", Description: "add two numbers"}, Add)
|
||||
|
|
@ -55,3 +57,5 @@ func ExampleSSEHandler() {
|
|||
|
||||
// Output: 3
|
||||
}
|
||||
|
||||
// !-ssehandler
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ type ToolHandler func(context.Context, *CallToolRequest) (*CallToolResult, error
|
|||
// - If the Out type is not the empty interface [any], it provides the
|
||||
// default output schema for the tool (which again may be overridden in
|
||||
// [AddTool]).
|
||||
// - The Out value is used to populate result.StructuredOutput.
|
||||
// - The Out value is used to populate result.StructuredContent.
|
||||
// - If [CallToolResult.Content] is unset, it is populated with the JSON
|
||||
// content of the output.
|
||||
// - An error result is treated as a tool error, rather than a protocol
|
||||
|
|
|
|||
|
|
@ -253,6 +253,74 @@ func ExampleAddTool_complexSchema() {
|
|||
// weather types: [sun partly_cloudy clouds rain snow]
|
||||
}
|
||||
|
||||
// !+contenttypes
|
||||
|
||||
func ExampleAddTool_contentTypes() {
|
||||
chartPNG := []byte("\x89PNG\r\n\x1a\n")
|
||||
summaryWAV := []byte("RIFF....WAVE")
|
||||
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "report"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Q3 revenue by region."},
|
||||
&mcp.ImageContent{Data: chartPNG, MIMEType: "image/png"},
|
||||
&mcp.AudioContent{Data: summaryWAV, MIMEType: "audio/wav"},
|
||||
// An embedded resource inlines the contents, so the client
|
||||
// needs no follow-up resources/read. Binary contents go in
|
||||
// ResourceContents.Blob instead of Text.
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "file:///reports/q3.csv",
|
||||
MIMEType: "text/csv",
|
||||
Text: "region,revenue\nEMEA,42\n",
|
||||
},
|
||||
},
|
||||
// A resource link only points at a resource. Prefer it when the
|
||||
// client may not need the contents, since an embedded resource
|
||||
// is transferred either way.
|
||||
&mcp.ResourceLink{URI: "file:///reports/q3.pdf", MIMEType: "application/pdf"},
|
||||
},
|
||||
}, nil, nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
session, err := connect(ctx, server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "report"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Clients type-switch over the content they receive.
|
||||
for _, content := range res.Content {
|
||||
switch c := content.(type) {
|
||||
case *mcp.TextContent:
|
||||
fmt.Println("text:", c.Text)
|
||||
case *mcp.ImageContent:
|
||||
fmt.Printf("image: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
||||
case *mcp.AudioContent:
|
||||
fmt.Printf("audio: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
||||
case *mcp.EmbeddedResource:
|
||||
fmt.Printf("embedded resource: %s (%s)\n", c.Resource.URI, c.Resource.MIMEType)
|
||||
case *mcp.ResourceLink:
|
||||
fmt.Printf("resource link: %s (%s)\n", c.URI, c.MIMEType)
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// text: Q3 revenue by region.
|
||||
// image: image/png, 8 bytes
|
||||
// audio: audio/wav, 12 bytes
|
||||
// embedded resource: file:///reports/q3.csv (text/csv)
|
||||
// resource link: file:///reports/q3.pdf (application/pdf)
|
||||
}
|
||||
|
||||
// !-contenttypes
|
||||
|
||||
func connect(ctx context.Context, server *mcp.Server) (*mcp.ClientSession, error) {
|
||||
t1, t2 := mcp.NewInMemoryTransports()
|
||||
if _, err := server.Connect(ctx, t1, nil); err != nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue