fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
"fmt"
"io"
"os/exec"
"syscall"
"time"
)
var defaultTerminateDuration = 5 * time.Second // mutable for testing
// A CommandTransport is a [Transport] that runs a command and communicates
// with it over stdin/stdout, using newline-delimited JSON.
type CommandTransport struct {
Command *exec.Cmd
// TerminateDuration controls how long Close waits after closing stdin
// for the process to exit before sending SIGTERM.
// If zero or negative, the default of 5s is used.
TerminateDuration time.Duration
}
// Connect starts the command, and connects to it over stdin/stdout.
func (t *CommandTransport) Connect(ctx context.Context) (Connection, error) {
stdout, err := t.Command.StdoutPipe()
if err != nil {
return nil, err
}
stdout = io.NopCloser(stdout) // close the connection by closing stdin, not stdout
stdin, err := t.Command.StdinPipe()
if err != nil {
return nil, err
}
if err := t.Command.Start(); err != nil {
return nil, err
}
td := t.TerminateDuration
if td <= 0 {
td = defaultTerminateDuration
}
return newIOConn(&pipeRWC{t.Command, stdout, stdin, td}), nil
}
// A pipeRWC is an io.ReadWriteCloser that communicates with a subprocess over
// stdin/stdout pipes.
type pipeRWC struct {
cmd *exec.Cmd
stdout io.ReadCloser
stdin io.WriteCloser
terminateDuration time.Duration
}
func (s *pipeRWC) Read(p []byte) (n int, err error) {
return s.stdout.Read(p)
}
func (s *pipeRWC) Write(p []byte) (n int, err error) {
return s.stdin.Write(p)
}
// Close closes the input stream to the child process, and awaits normal
// termination of the command. If the command does not exit, it is signalled to
// terminate, and then eventually killed.
func (s *pipeRWC) Close() error {
// Spec:
// "For the stdio transport, the client SHOULD initiate shutdown by:...
// "...First, closing the input stream to the child process (the server)"
if err := s.stdin.Close(); err != nil {
return fmt.Errorf("closing stdin: %v", err)
}
resChan := make(chan error, 1)
go func() {
resChan <- s.cmd.Wait()
}()
// "...Waiting for the server to exit, or sending SIGTERM if the server does not exit within a reasonable time"
wait := func() (error, bool) {
select {
case err := <-resChan:
return err, true
case <-time.After(s.terminateDuration):
}
return nil, false
}
if err, ok := wait(); ok {
return err
}
// Note the condition here: if sending SIGTERM fails, don't wait and just
// move on to SIGKILL.
if err := s.cmd.Process.Signal(syscall.SIGTERM); err == nil {
if err, ok := wait(); ok {
return err
}
}
// "...Sending SIGKILL if the server does not exit within a reasonable time after SIGTERM"
if err := s.cmd.Process.Kill(); err != nil {
return err
}
if err, ok := wait(); ok {
return err
}
return fmt.Errorf("unresponsive subprocess")
}
+410
View File
@@ -0,0 +1,410 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// TODO(findleyr): update JSON marshalling of all content types to preserve required fields.
// (See [TextContent.MarshalJSON], which handles this for text content).
package mcp
import (
"encoding/json"
"fmt"
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
)
// A Content is a [TextContent], [ImageContent], [AudioContent],
// [ResourceLink], [EmbeddedResource], [ToolUseContent], or [ToolResultContent].
//
// Note: [ToolUseContent] and [ToolResultContent] are only valid in sampling
// message contexts (CreateMessageParams/CreateMessageResult).
type Content interface {
MarshalJSON() ([]byte, error)
fromWire(*wireContent)
}
// TextContent is a textual content.
type TextContent struct {
Text string
Meta Meta
Annotations *Annotations
}
func (c *TextContent) MarshalJSON() ([]byte, error) {
// Custom wire format to ensure the required "text" field is always included, even when empty.
wire := struct {
Type string `json:"type"`
Text string `json:"text"`
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
}{
Type: "text",
Text: c.Text,
Meta: c.Meta,
Annotations: c.Annotations,
}
return json.Marshal(wire)
}
func (c *TextContent) fromWire(wire *wireContent) {
c.Text = wire.Text
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}
// ImageContent contains base64-encoded image data.
type ImageContent struct {
Meta Meta
Annotations *Annotations
Data []byte // base64-encoded
MIMEType string
}
func (c *ImageContent) MarshalJSON() ([]byte, error) {
// Custom wire format to ensure required fields are always included, even when empty.
data := c.Data
if data == nil {
data = []byte{}
}
wire := imageAudioWire{
Type: "image",
MIMEType: c.MIMEType,
Data: data,
Meta: c.Meta,
Annotations: c.Annotations,
}
return json.Marshal(wire)
}
func (c *ImageContent) fromWire(wire *wireContent) {
c.MIMEType = wire.MIMEType
c.Data = wire.Data
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}
// AudioContent contains base64-encoded audio data.
type AudioContent struct {
Data []byte
MIMEType string
Meta Meta
Annotations *Annotations
}
func (c AudioContent) MarshalJSON() ([]byte, error) {
// Custom wire format to ensure required fields are always included, even when empty.
data := c.Data
if data == nil {
data = []byte{}
}
wire := imageAudioWire{
Type: "audio",
MIMEType: c.MIMEType,
Data: data,
Meta: c.Meta,
Annotations: c.Annotations,
}
return json.Marshal(wire)
}
func (c *AudioContent) fromWire(wire *wireContent) {
c.MIMEType = wire.MIMEType
c.Data = wire.Data
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}
// Custom wire format to ensure required fields are always included, even when empty.
type imageAudioWire struct {
Type string `json:"type"`
MIMEType string `json:"mimeType"`
Data []byte `json:"data"`
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
}
// ResourceLink is a link to a resource
type ResourceLink struct {
URI string
Name string
Title string
Description string
MIMEType string
Size *int64
Meta Meta
Annotations *Annotations
// Icons for the resource link, if any.
Icons []Icon `json:"icons,omitempty"`
}
func (c *ResourceLink) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "resource_link",
URI: c.URI,
Name: c.Name,
Title: c.Title,
Description: c.Description,
MIMEType: c.MIMEType,
Size: c.Size,
Meta: c.Meta,
Annotations: c.Annotations,
Icons: c.Icons,
})
}
func (c *ResourceLink) fromWire(wire *wireContent) {
c.URI = wire.URI
c.Name = wire.Name
c.Title = wire.Title
c.Description = wire.Description
c.MIMEType = wire.MIMEType
c.Size = wire.Size
c.Meta = wire.Meta
c.Annotations = wire.Annotations
c.Icons = wire.Icons
}
// EmbeddedResource contains embedded resources.
type EmbeddedResource struct {
Resource *ResourceContents
Meta Meta
Annotations *Annotations
}
func (c *EmbeddedResource) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "resource",
Resource: c.Resource,
Meta: c.Meta,
Annotations: c.Annotations,
})
}
func (c *EmbeddedResource) fromWire(wire *wireContent) {
c.Resource = wire.Resource
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}
// ToolUseContent represents a request from the assistant to invoke a tool.
// This content type is only valid in sampling messages.
type ToolUseContent struct {
// ID is a unique identifier for this tool use, used to match with ToolResultContent.
ID string
// Name is the name of the tool to invoke.
Name string
// Input contains the tool arguments as a JSON object.
Input map[string]any
Meta Meta
}
func (c *ToolUseContent) MarshalJSON() ([]byte, error) {
input := c.Input
if input == nil {
input = map[string]any{}
}
wire := struct {
Type string `json:"type"`
ID string `json:"id"`
Name string `json:"name"`
Input map[string]any `json:"input"`
Meta Meta `json:"_meta,omitempty"`
}{
Type: "tool_use",
ID: c.ID,
Name: c.Name,
Input: input,
Meta: c.Meta,
}
return json.Marshal(wire)
}
func (c *ToolUseContent) fromWire(wire *wireContent) {
c.ID = wire.ID
c.Name = wire.Name
c.Input = wire.Input
c.Meta = wire.Meta
}
// ToolResultContent represents the result of a tool invocation.
// This content type is only valid in sampling messages with role "user".
type ToolResultContent struct {
// ToolUseID references the ID from the corresponding ToolUseContent.
ToolUseID string
// Content holds the unstructured result of the tool call.
Content []Content
// StructuredContent holds an optional structured result as a JSON object.
StructuredContent any
// IsError indicates whether the tool call ended in an error.
IsError bool
Meta Meta
}
func (c *ToolResultContent) MarshalJSON() ([]byte, error) {
// Marshal nested content
var contentWire []*wireContent
for _, content := range c.Content {
data, err := content.MarshalJSON()
if err != nil {
return nil, err
}
var w wireContent
if err := internaljson.Unmarshal(data, &w); err != nil {
return nil, err
}
contentWire = append(contentWire, &w)
}
if contentWire == nil {
contentWire = []*wireContent{} // avoid JSON null
}
wire := struct {
Type string `json:"type"`
ToolUseID string `json:"toolUseId"`
Content []*wireContent `json:"content"`
StructuredContent any `json:"structuredContent,omitempty"`
IsError bool `json:"isError,omitempty"`
Meta Meta `json:"_meta,omitempty"`
}{
Type: "tool_result",
ToolUseID: c.ToolUseID,
Content: contentWire,
StructuredContent: c.StructuredContent,
IsError: c.IsError,
Meta: c.Meta,
}
return json.Marshal(wire)
}
func (c *ToolResultContent) fromWire(wire *wireContent) {
c.ToolUseID = wire.ToolUseID
c.StructuredContent = wire.StructuredContent
c.IsError = wire.IsError
c.Meta = wire.Meta
// Content is handled separately in contentFromWire due to nested content
}
// ResourceContents contains the contents of a specific resource or
// sub-resource.
type ResourceContents struct {
URI string `json:"uri"`
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Blob []byte `json:"blob,omitzero"`
Meta Meta `json:"_meta,omitempty"`
}
// wireContent is the wire format for content.
// It represents the protocol types TextContent, ImageContent, AudioContent,
// ResourceLink, EmbeddedResource, ToolUseContent, and ToolResultContent.
// The Type field distinguishes them. In the protocol, each type has a constant
// value for the field.
type wireContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"` // TextContent
MIMEType string `json:"mimeType,omitempty"` // ImageContent, AudioContent, ResourceLink
Data []byte `json:"data,omitempty"` // ImageContent, AudioContent
Resource *ResourceContents `json:"resource,omitempty"` // EmbeddedResource
URI string `json:"uri,omitempty"` // ResourceLink
Name string `json:"name,omitempty"` // ResourceLink, ToolUseContent
Title string `json:"title,omitempty"` // ResourceLink
Description string `json:"description,omitempty"` // ResourceLink
Size *int64 `json:"size,omitempty"` // ResourceLink
Meta Meta `json:"_meta,omitempty"` // all types
Annotations *Annotations `json:"annotations,omitempty"` // all types except ToolUseContent, ToolResultContent
Icons []Icon `json:"icons,omitempty"` // ResourceLink
ID string `json:"id,omitempty"` // ToolUseContent
Input map[string]any `json:"input,omitempty"` // ToolUseContent
ToolUseID string `json:"toolUseId,omitempty"` // ToolResultContent
NestedContent []*wireContent `json:"content,omitempty"` // ToolResultContent
StructuredContent any `json:"structuredContent,omitempty"` // ToolResultContent
IsError bool `json:"isError,omitempty"` // ToolResultContent
}
// unmarshalContent unmarshals JSON that is either a single content object or
// an array of content objects. A single object is wrapped in a one-element slice.
func unmarshalContent(raw json.RawMessage, allow map[string]bool) ([]Content, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, fmt.Errorf("nil content")
}
// Try array first, then fall back to single object.
var wires []*wireContent
if err := internaljson.Unmarshal(raw, &wires); err == nil {
return contentsFromWire(wires, allow)
}
var wire wireContent
if err := internaljson.Unmarshal(raw, &wire); err != nil {
return nil, err
}
c, err := contentFromWire(&wire, allow)
if err != nil {
return nil, err
}
return []Content{c}, nil
}
func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) {
blocks := make([]Content, 0, len(wires))
for _, wire := range wires {
block, err := contentFromWire(wire, allow)
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}
func contentFromWire(wire *wireContent, allow map[string]bool) (Content, error) {
if wire == nil {
return nil, fmt.Errorf("nil content")
}
if allow != nil && !allow[wire.Type] {
return nil, fmt.Errorf("invalid content type %q", wire.Type)
}
switch wire.Type {
case "text":
v := new(TextContent)
v.fromWire(wire)
return v, nil
case "image":
v := new(ImageContent)
v.fromWire(wire)
return v, nil
case "audio":
v := new(AudioContent)
v.fromWire(wire)
return v, nil
case "resource_link":
v := new(ResourceLink)
v.fromWire(wire)
return v, nil
case "resource":
v := new(EmbeddedResource)
v.fromWire(wire)
return v, nil
case "tool_use":
v := new(ToolUseContent)
v.fromWire(wire)
return v, nil
case "tool_result":
v := new(ToolResultContent)
v.fromWire(wire)
// Handle nested content - tool_result content can contain text, image, audio,
// resource_link, and resource (same as CallToolResult.content)
if wire.NestedContent != nil {
toolResultContentAllow := map[string]bool{
"text": true, "image": true, "audio": true,
"resource_link": true, "resource": true,
}
nestedContent, err := contentsFromWire(wire.NestedContent, toolResultContentAllow)
if err != nil {
return nil, fmt.Errorf("tool_result nested content: %w", err)
}
v.Content = nestedContent
}
return v, nil
}
return nil, fmt.Errorf("unrecognized content type %q", wire.Type)
}
+436
View File
@@ -0,0 +1,436 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// This file is for SSE events.
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events.
package mcp
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"iter"
"maps"
"net/http"
"slices"
"strings"
"sync"
)
// If true, MemoryEventStore will do frequent validation to check invariants, slowing it down.
// Enable for debugging.
const validateMemoryEventStore = false
// An Event is a server-sent event.
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields.
type Event struct {
Name string // the "event" field
ID string // the "id" field
Data []byte // the "data" field
Retry string // the "retry" field
}
// Empty reports whether the Event is empty.
func (e Event) Empty() bool {
return e.Name == "" && e.ID == "" && len(e.Data) == 0 && e.Retry == ""
}
// writeEvent writes the event to w, and flushes.
func writeEvent(w io.Writer, evt Event) (int, error) {
var b bytes.Buffer
if evt.Name != "" {
fmt.Fprintf(&b, "event: %s\n", evt.Name)
}
if evt.ID != "" {
fmt.Fprintf(&b, "id: %s\n", evt.ID)
}
if evt.Retry != "" {
fmt.Fprintf(&b, "retry: %s\n", evt.Retry)
}
fmt.Fprintf(&b, "data: %s\n\n", string(evt.Data))
n, err := w.Write(b.Bytes())
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
return n, err
}
// scanEvents iterates SSE events in the given scanner. The iterated error is
// terminal: if encountered, the stream is corrupt or broken and should no
// longer be used.
//
// TODO(rfindley): consider a different API here that makes failure modes more
// apparent.
func scanEvents(r io.Reader) iter.Seq2[Event, error] {
reader := bufio.NewReader(r)
// TODO: investigate proper behavior when events are out of order, or have
// non-standard names.
var (
eventKey = []byte("event")
idKey = []byte("id")
dataKey = []byte("data")
retryKey = []byte("retry")
)
return func(yield func(Event, error) bool) {
// iterate event from the wire.
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#examples
//
// - `key: value` line records.
// - Consecutive `data: ...` fields are joined with newlines.
// - Unrecognized fields are ignored. Since we only care about 'event', 'id', and
// 'data', these are the only three we consider.
// - Lines starting with ":" are ignored.
// - Records are terminated with two consecutive newlines.
var (
evt Event
dataBuf *bytes.Buffer // if non-nil, preceding field was also data
)
yieldEvent := func() bool {
if dataBuf != nil {
evt.Data = dataBuf.Bytes()
dataBuf = nil
}
if evt.Empty() {
return true
}
if !yield(evt, nil) {
return false
}
evt = Event{}
return true
}
for {
line, err := reader.ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) {
yield(Event{}, fmt.Errorf("error reading event: %v", err))
return
}
line = bytes.TrimRight(line, "\r\n")
isEOF := errors.Is(err, io.EOF)
if len(line) == 0 {
if !yieldEvent() {
return
}
if isEOF {
return
}
continue
}
before, after, found := bytes.Cut(line, []byte{':'})
if !found {
yield(Event{}, fmt.Errorf("%w: malformed line in SSE stream: %q", errMalformedEvent, string(line)))
return
}
switch {
case bytes.Equal(before, eventKey):
evt.Name = strings.TrimSpace(string(after))
case bytes.Equal(before, idKey):
evt.ID = strings.TrimSpace(string(after))
case bytes.Equal(before, retryKey):
evt.Retry = strings.TrimSpace(string(after))
case bytes.Equal(before, dataKey):
data := bytes.TrimSpace(after)
if dataBuf == nil {
dataBuf = new(bytes.Buffer)
} else {
dataBuf.WriteByte('\n')
}
dataBuf.Write(data)
}
if isEOF {
yieldEvent()
return
}
}
}
}
// An EventStore tracks data for SSE streams.
// A single EventStore suffices for all sessions, since session IDs are
// globally unique. So one EventStore can be created per process, for
// all Servers in the process.
// Such a store is able to bound resource usage for the entire process.
//
// All of an EventStore's methods must be safe for use by multiple goroutines.
type EventStore interface {
// Open is called when a new stream is created. It may be used to ensure that
// the underlying data structure for the stream is initialized, making it
// ready to store and replay event streams.
Open(_ context.Context, sessionID, streamID string) error
// Append appends data for an outgoing event to given stream, which is part of the
// given session.
Append(_ context.Context, sessionID, streamID string, data []byte) error
// After returns an iterator over the data for the given session and stream, beginning
// just after the given index.
//
// Once the iterator yields a non-nil error, it will stop.
// After's iterator must return an error immediately if any data after index was
// dropped; it must not return partial results.
// The stream must have been opened previously (see [EventStore.Open]).
After(_ context.Context, sessionID, streamID string, index int) iter.Seq2[[]byte, error]
// SessionClosed informs the store that the given session is finished, along
// with all of its streams.
//
// A store cannot rely on this method being called for cleanup. It should institute
// additional mechanisms, such as timeouts, to reclaim storage.
SessionClosed(_ context.Context, sessionID string) error
// There is no StreamClosed method. A server doesn't know when a stream is finished, because
// the client can always send a GET with a Last-Event-ID referring to the stream.
}
// A dataList is a list of []byte.
// The zero dataList is ready to use.
type dataList struct {
size int // total size of data bytes
first int // the stream index of the first element in data
data [][]byte
}
func (dl *dataList) appendData(d []byte) {
// Empty data consumes memory but doesn't increment size. However, it should
// be rare.
dl.data = append(dl.data, d)
dl.size += len(d)
}
// removeFirst removes the first data item in dl, returning the size of the item.
// It panics if dl is empty.
func (dl *dataList) removeFirst() int {
if len(dl.data) == 0 {
panic("empty dataList")
}
r := len(dl.data[0])
dl.size -= r
dl.data[0] = nil // help GC
dl.data = dl.data[1:]
dl.first++
return r
}
// A MemoryEventStore is an [EventStore] backed by memory.
type MemoryEventStore struct {
mu sync.Mutex
maxBytes int // max total size of all data
nBytes int // current total size of all data
store map[string]map[string]*dataList // session ID -> stream ID -> *dataList
}
// MemoryEventStoreOptions are options for a [MemoryEventStore].
type MemoryEventStoreOptions struct{}
// MaxBytes returns the maximum number of bytes that the store will retain before
// purging data.
func (s *MemoryEventStore) MaxBytes() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.maxBytes
}
// SetMaxBytes sets the maximum number of bytes the store will retain before purging
// data. The argument must not be negative. If it is zero, a suitable default will be used.
// SetMaxBytes can be called at any time. The size of the store will be adjusted
// immediately.
func (s *MemoryEventStore) SetMaxBytes(n int) {
s.mu.Lock()
defer s.mu.Unlock()
switch {
case n < 0:
panic("negative argument")
case n == 0:
s.maxBytes = defaultMaxBytes
default:
s.maxBytes = n
}
s.purge()
}
const defaultMaxBytes = 10 << 20 // 10 MiB
// NewMemoryEventStore creates a [MemoryEventStore] with the default value
// for MaxBytes.
func NewMemoryEventStore(opts *MemoryEventStoreOptions) *MemoryEventStore {
return &MemoryEventStore{
maxBytes: defaultMaxBytes,
store: make(map[string]map[string]*dataList),
}
}
// Open implements [EventStore.Open]. It ensures that the underlying data
// structures for the given session are initialized and ready for use.
func (s *MemoryEventStore) Open(_ context.Context, sessionID, streamID string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.init(sessionID, streamID)
return nil
}
// init is an internal helper function that ensures the nested map structure for a
// given sessionID and streamID exists, creating it if necessary. It returns the
// dataList associated with the specified IDs.
// Requires s.mu.
func (s *MemoryEventStore) init(sessionID, streamID string) *dataList {
streamMap, ok := s.store[sessionID]
if !ok {
streamMap = make(map[string]*dataList)
s.store[sessionID] = streamMap
}
dl, ok := streamMap[streamID]
if !ok {
dl = &dataList{}
streamMap[streamID] = dl
}
return dl
}
// Append implements [EventStore.Append] by recording data in memory.
func (s *MemoryEventStore) Append(_ context.Context, sessionID, streamID string, data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
dl := s.init(sessionID, streamID)
// Purge before adding, so at least the current data item will be present.
// (That could result in nBytes > maxBytes, but we'll live with that.)
s.purge()
dl.appendData(data)
s.nBytes += len(data)
return nil
}
// ErrEventsPurged is the error that [EventStore.After] should return if the event just after the
// index is no longer available.
var ErrEventsPurged = errors.New("data purged")
// errMalformedEvent is returned when an SSE event cannot be parsed due to format violations.
// This is a hard error indicating corrupted data or protocol violations, as opposed to
// transient I/O errors which may be retryable.
var errMalformedEvent = errors.New("malformed event")
// After implements [EventStore.After].
func (s *MemoryEventStore) After(_ context.Context, sessionID, streamID string, index int) iter.Seq2[[]byte, error] {
// Return the data items to yield.
// We must copy, because dataList.removeFirst nils out slice elements.
copyData := func() ([][]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
streamMap, ok := s.store[sessionID]
if !ok {
return nil, fmt.Errorf("MemoryEventStore.After: unknown session ID %q", sessionID)
}
dl, ok := streamMap[streamID]
if !ok {
return nil, fmt.Errorf("MemoryEventStore.After: unknown stream ID %v in session %q", streamID, sessionID)
}
start := index + 1
if dl.first > start {
return nil, fmt.Errorf("MemoryEventStore.After: index %d, stream ID %v, session %q: %w",
index, streamID, sessionID, ErrEventsPurged)
}
return slices.Clone(dl.data[start-dl.first:]), nil
}
return func(yield func([]byte, error) bool) {
ds, err := copyData()
if err != nil {
yield(nil, err)
return
}
for _, d := range ds {
if !yield(d, nil) {
return
}
}
}
}
// SessionClosed implements [EventStore.SessionClosed].
func (s *MemoryEventStore) SessionClosed(_ context.Context, sessionID string) error {
s.mu.Lock()
defer s.mu.Unlock()
for _, dl := range s.store[sessionID] {
s.nBytes -= dl.size
}
delete(s.store, sessionID)
s.validate()
return nil
}
// purge removes data until no more than s.maxBytes bytes are in use.
// It must be called with s.mu held.
func (s *MemoryEventStore) purge() {
// Remove the first element of every dataList until below the max.
for s.nBytes > s.maxBytes {
changed := false
for _, sm := range s.store {
for _, dl := range sm {
if dl.size > 0 {
r := dl.removeFirst()
if r > 0 {
changed = true
s.nBytes -= r
}
}
}
}
if !changed {
panic("no progress during purge")
}
}
s.validate()
}
// validate checks that the store's data structures are valid.
// It must be called with s.mu held.
func (s *MemoryEventStore) validate() {
if !validateMemoryEventStore {
return
}
// Check that we're accounting for the size correctly.
n := 0
for _, sm := range s.store {
for _, dl := range sm {
for _, d := range dl.data {
n += len(d)
}
}
}
if n != s.nBytes {
panic("sizes don't add up")
}
}
// debugString returns a string containing the state of s.
// Used in tests.
func (s *MemoryEventStore) debugString() string {
s.mu.Lock()
defer s.mu.Unlock()
var b strings.Builder
for i, sess := range slices.Sorted(maps.Keys(s.store)) {
if i > 0 {
fmt.Fprintf(&b, "; ")
}
sm := s.store[sess]
for i, sid := range slices.Sorted(maps.Keys(sm)) {
if i > 0 {
fmt.Fprintf(&b, "; ")
}
dl := sm[sid]
fmt.Fprintf(&b, "%s %s first=%d", sess, sid, dl.first)
for _, d := range dl.data {
fmt.Fprintf(&b, " %s", d)
}
}
}
return b.String()
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"iter"
"maps"
"slices"
)
// This file contains implementations that are common to all features.
// A feature is an item provided to a peer. In the 2025-03-26 spec,
// the features are prompt, tool, resource and root.
// A featureSet is a collection of features of type T.
// Every feature has a unique ID, and the spec never mentions
// an ordering for the List calls, so what it calls a "list" is actually a set.
//
// An alternative implementation would use an ordered map, but that's probably
// not necessary as adds and removes are rare, and usually batched.
type featureSet[T any] struct {
uniqueID func(T) string
features map[string]T
sortedKeys []string // lazily computed; nil after add or remove
}
// newFeatureSet creates a new featureSet for features of type T.
// The argument function should return the unique ID for a single feature.
func newFeatureSet[T any](uniqueIDFunc func(T) string) *featureSet[T] {
return &featureSet[T]{
uniqueID: uniqueIDFunc,
features: make(map[string]T),
}
}
// add adds each feature to the set if it is not present,
// or replaces an existing feature.
func (s *featureSet[T]) add(fs ...T) {
for _, f := range fs {
s.features[s.uniqueID(f)] = f
}
s.sortedKeys = nil
}
// remove removes all features with the given uids from the set if present,
// and returns whether any were removed.
// It is not an error to remove a nonexistent feature.
func (s *featureSet[T]) remove(uids ...string) bool {
changed := false
for _, uid := range uids {
if _, ok := s.features[uid]; ok {
changed = true
delete(s.features, uid)
}
}
if changed {
s.sortedKeys = nil
}
return changed
}
// get returns the feature with the given uid.
// If there is none, it returns zero, false.
func (s *featureSet[T]) get(uid string) (T, bool) {
t, ok := s.features[uid]
return t, ok
}
// len returns the number of features in the set.
func (s *featureSet[T]) len() int { return len(s.features) }
// all returns an iterator over of all the features in the set
// sorted by unique ID.
func (s *featureSet[T]) all() iter.Seq[T] {
s.sortKeys()
return func(yield func(T) bool) {
s.yieldFrom(0, yield)
}
}
// above returns an iterator over features in the set whose unique IDs are
// greater than `uid`, in ascending ID order.
func (s *featureSet[T]) above(uid string) iter.Seq[T] {
s.sortKeys()
index, found := slices.BinarySearch(s.sortedKeys, uid)
if found {
index++
}
return func(yield func(T) bool) {
s.yieldFrom(index, yield)
}
}
// sortKeys is a helper that maintains a sorted list of feature IDs. It
// computes this list lazily upon its first call after a modification, or
// if it's nil.
func (s *featureSet[T]) sortKeys() {
if s.sortedKeys != nil {
return
}
s.sortedKeys = slices.Sorted(maps.Keys(s.features))
}
// yieldFrom is a helper that iterates over the features in the set,
// starting at the given index, and calls the yield function for each one.
func (s *featureSet[T]) yieldFrom(index int, yield func(T) bool) {
for i := index; i < len(s.sortedKeys); i++ {
if !yield(s.features[s.sortedKeys[i]]) {
return
}
}
}
+201
View File
@@ -0,0 +1,201 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"bytes"
"cmp"
"context"
"encoding/json"
"log/slog"
"slices"
"sync"
"time"
)
// Logging levels.
const (
LevelDebug = slog.LevelDebug
LevelInfo = slog.LevelInfo
LevelNotice = (slog.LevelInfo + slog.LevelWarn) / 2
LevelWarning = slog.LevelWarn
LevelError = slog.LevelError
LevelCritical = slog.LevelError + 4
LevelAlert = slog.LevelError + 8
LevelEmergency = slog.LevelError + 12
)
var slogToMCP = map[slog.Level]LoggingLevel{
LevelDebug: "debug",
LevelInfo: "info",
LevelNotice: "notice",
LevelWarning: "warning",
LevelError: "error",
LevelCritical: "critical",
LevelAlert: "alert",
LevelEmergency: "emergency",
}
var mcpToSlog = make(map[LoggingLevel]slog.Level)
func init() {
for sl, ml := range slogToMCP {
mcpToSlog[ml] = sl
}
}
func slogLevelToMCP(sl slog.Level) LoggingLevel {
if ml, ok := slogToMCP[sl]; ok {
return ml
}
return "debug" // for lack of a better idea
}
func mcpLevelToSlog(ll LoggingLevel) slog.Level {
if sl, ok := mcpToSlog[ll]; ok {
return sl
}
// TODO: is there a better default?
return LevelDebug
}
// compareLevels behaves like [cmp.Compare] for [LoggingLevel]s.
func compareLevels(l1, l2 LoggingLevel) int {
return cmp.Compare(mcpLevelToSlog(l1), mcpLevelToSlog(l2))
}
// LoggingHandlerOptions are options for a LoggingHandler.
type LoggingHandlerOptions struct {
// The value for the "logger" field of logging notifications.
LoggerName string
// Limits the rate at which log messages are sent.
// Excess messages are dropped.
// If zero, there is no rate limiting.
MinInterval time.Duration
}
// A LoggingHandler is a [slog.Handler] for MCP.
type LoggingHandler struct {
opts LoggingHandlerOptions
ss *ServerSession
// Ensures that the buffer reset is atomic with the write (see Handle).
// A pointer so that clones share the mutex. See
// https://github.com/golang/example/blob/master/slog-handler-guide/README.md#getting-the-mutex-right.
mu *sync.Mutex
lastMessageSent time.Time // for rate-limiting
buf *bytes.Buffer
handler slog.Handler
}
// ensureLogger returns l if non-nil, otherwise a discard logger.
func ensureLogger(l *slog.Logger) *slog.Logger {
if l != nil {
return l
}
return slog.New(slog.DiscardHandler)
}
// NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a
// [slog.JSONHandler].
func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler {
var buf bytes.Buffer
jsonHandler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
// Remove level: it appears in LoggingMessageParams.
if a.Key == slog.LevelKey {
return slog.Attr{}
}
return a
},
})
lh := &LoggingHandler{
ss: ss,
mu: new(sync.Mutex),
buf: &buf,
handler: jsonHandler,
}
if opts != nil {
lh.opts = *opts
}
return lh
}
// Enabled implements [slog.Handler.Enabled] by comparing level to the [ServerSession]'s level.
func (h *LoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
// This is also checked in ServerSession.LoggingMessage, so checking it here
// is just an optimization that skips building the JSON.
h.ss.mu.Lock()
mcpLevel := h.ss.state.LogLevel
h.ss.mu.Unlock()
return level >= mcpLevelToSlog(mcpLevel)
}
// WithAttrs implements [slog.Handler.WithAttrs].
func (h *LoggingHandler) WithAttrs(as []slog.Attr) slog.Handler {
h2 := *h
h2.handler = h.handler.WithAttrs(as)
return &h2
}
// WithGroup implements [slog.Handler.WithGroup].
func (h *LoggingHandler) WithGroup(name string) slog.Handler {
h2 := *h
h2.handler = h.handler.WithGroup(name)
return &h2
}
// Handle implements [slog.Handler.Handle] by writing the Record to a JSONHandler,
// then calling [ServerSession.LoggingMessage] with the result.
func (h *LoggingHandler) Handle(ctx context.Context, r slog.Record) error {
err := h.handle(ctx, r)
// TODO(jba): find a way to surface the error.
// The return value will probably be ignored.
return err
}
func (h *LoggingHandler) handle(ctx context.Context, r slog.Record) error {
// Observe the rate limit.
// TODO(jba): use golang.org/x/time/rate.
h.mu.Lock()
skip := time.Since(h.lastMessageSent) < h.opts.MinInterval
h.mu.Unlock()
if skip {
return nil
}
var err error
var data json.RawMessage
// Make the buffer reset atomic with the record write.
// We are careful here in the unlikely event that the handler panics.
// We don't want to hold the lock for the entire function, because Notify is
// an I/O operation.
// This can result in out-of-order delivery.
func() {
h.mu.Lock()
defer h.mu.Unlock()
h.buf.Reset()
err = h.handler.Handle(ctx, r)
// Clone the buffer as Bytes() references the internal buffer.
data = json.RawMessage(slices.Clone(h.buf.Bytes()))
}()
if err != nil {
return err
}
h.mu.Lock()
h.lastMessageSent = time.Now()
h.mu.Unlock()
params := &LoggingMessageParams{
Logger: h.opts.LoggerName,
Level: slogLevelToMCP(r.Level),
Data: data,
}
// We pass the argument context to Notify, even though slog.Handler.Handle's
// documentation says not to.
// In this case logging is a service to clients, not a means for debugging the
// server, so we want to cancel the log message.
return h.ss.Log(ctx, params)
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// The mcp package provides an SDK for writing model context protocol clients
// and servers.
//
// To get started, create either a [Client] or [Server], add features to it
// using `AddXXX` functions, and connect it to a peer using a [Transport].
//
// For example, to run a simple server on the [StdioTransport]:
//
// server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
//
// // Using the generic AddTool automatically populates the the input and output
// // schema of the tool.
// type args struct {
// Name string `json:"name" jsonschema:"the person to greet"`
// }
// mcp.AddTool(server, &mcp.Tool{
// Name: "greet",
// Description: "say hi",
// }, func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
// return &mcp.CallToolResult{
// Content: []mcp.Content{
// &mcp.TextContent{Text: "Hi " + args.Name},
// },
// }, nil, nil
// })
//
// // Run the server on the stdio transport.
// if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
// log.Printf("Server failed: %v", err)
// }
//
// To connect to this server, use the [CommandTransport]:
//
// client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
// transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
// session, err := client.Connect(ctx, transport, nil)
// if err != nil {
// log.Fatal(err)
// }
// defer session.Close()
//
// params := &mcp.CallToolParams{
// Name: "greet",
// Arguments: map[string]any{"name": "you"},
// }
// res, err := session.CallTool(ctx, params)
// if err != nil {
// log.Fatalf("CallTool failed: %v", err)
// }
//
// # Clients, servers, and sessions
//
// In this SDK, both a [Client] and [Server] may handle many concurrent
// connections. Each time a client or server is connected to a peer using a
// [Transport], it creates a new session (either a [ClientSession] or
// [ServerSession]):
//
// Client Server
// ⇅ (jsonrpc2) ⇅
// ClientSession ⇄ Client Transport ⇄ Server Transport ⇄ ServerSession
//
// The session types expose an API to interact with its peer. For example,
// [ClientSession.CallTool] or [ServerSession.ListRoots].
//
// # Adding features
//
// Add MCP servers to your Client or Server using AddXXX methods (for example
// [Client.AddRoot] or [Server.AddPrompt]). If any peers are connected when
// AddXXX is called, they will receive a corresponding change notification
// (for example notifications/roots/list_changed).
//
// Adding tools is special: tools may be bound to ordinary Go functions by
// using the top-level generic [AddTool] function, which allows specifying an
// input and output type. When AddTool is used, the tool's input schema and
// output schema are automatically populated, and inputs are automatically
// validated. As a special case, if the output type is 'any', no output schema
// is generated.
//
// func double(_ context.Context, _ *mcp.CallToolRequest, in In) (*mcp.CallToolResult, Out, error) {
// return nil, Out{Answer: 2*in.Number}, nil
// }
// ...
// mcp.AddTool(server, &mcp.Tool{Name: "double"}, double)
package mcp
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
)
// A PromptHandler handles a call to prompts/get.
type PromptHandler func(context.Context, *GetPromptRequest) (*GetPromptResult, error)
type serverPrompt struct {
prompt *Prompt
handler PromptHandler
}
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// This file holds the request types.
package mcp
type (
CallToolRequest = ServerRequest[*CallToolParamsRaw]
CompleteRequest = ServerRequest[*CompleteParams]
GetPromptRequest = ServerRequest[*GetPromptParams]
InitializedRequest = ServerRequest[*InitializedParams]
ListPromptsRequest = ServerRequest[*ListPromptsParams]
ListResourcesRequest = ServerRequest[*ListResourcesParams]
ListResourceTemplatesRequest = ServerRequest[*ListResourceTemplatesParams]
ListToolsRequest = ServerRequest[*ListToolsParams]
ProgressNotificationServerRequest = ServerRequest[*ProgressNotificationParams]
ReadResourceRequest = ServerRequest[*ReadResourceParams]
RootsListChangedRequest = ServerRequest[*RootsListChangedParams]
SubscribeRequest = ServerRequest[*SubscribeParams]
UnsubscribeRequest = ServerRequest[*UnsubscribeParams]
)
type (
CreateMessageRequest = ClientRequest[*CreateMessageParams]
CreateMessageWithToolsRequest = ClientRequest[*CreateMessageWithToolsParams]
ElicitRequest = ClientRequest[*ElicitParams]
initializedClientRequest = ClientRequest[*InitializedParams]
InitializeRequest = ClientRequest[*InitializeParams]
ListRootsRequest = ClientRequest[*ListRootsParams]
LoggingMessageRequest = ClientRequest[*LoggingMessageParams]
ProgressNotificationClientRequest = ClientRequest[*ProgressNotificationParams]
PromptListChangedRequest = ClientRequest[*PromptListChangedParams]
ResourceListChangedRequest = ClientRequest[*ResourceListChangedParams]
ResourceUpdatedNotificationRequest = ClientRequest[*ResourceUpdatedNotificationParams]
ToolListChangedRequest = ClientRequest[*ToolListChangedParams]
ElicitationCompleteNotificationRequest = ClientRequest[*ElicitationCompleteParams]
)
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/modelcontextprotocol/go-sdk/internal/util"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/yosida95/uritemplate/v3"
)
// A serverResource associates a Resource with its handler.
type serverResource struct {
resource *Resource
handler ResourceHandler
}
// A serverResourceTemplate associates a ResourceTemplate with its handler.
type serverResourceTemplate struct {
resourceTemplate *ResourceTemplate
handler ResourceHandler
}
// A ResourceHandler is a function that reads a resource.
// It will be called when the client calls [ClientSession.ReadResource].
// If it cannot find the resource, it should return the result of calling [ResourceNotFoundError].
type ResourceHandler func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error)
// ResourceNotFoundError returns an error indicating that a resource being read could
// not be found.
func ResourceNotFoundError(uri string) error {
return &jsonrpc.Error{
Code: CodeResourceNotFound,
Message: "Resource not found",
Data: json.RawMessage(fmt.Sprintf(`{"uri":%q}`, uri)),
}
}
// readFileResource reads from the filesystem at a URI relative to dirFilepath, respecting
// the roots.
// dirFilepath and rootFilepaths are absolute filesystem paths.
func readFileResource(rawURI, dirFilepath string, rootFilepaths []string) ([]byte, error) {
uriFilepath, err := computeURIFilepath(rawURI, dirFilepath, rootFilepaths)
if err != nil {
return nil, err
}
var data []byte
err = withFile(dirFilepath, uriFilepath, func(f *os.File) error {
var err error
data, err = io.ReadAll(f)
return err
})
if os.IsNotExist(err) {
err = ResourceNotFoundError(rawURI)
}
return data, err
}
// computeURIFilepath returns a path relative to dirFilepath.
// The dirFilepath and rootFilepaths are absolute file paths.
func computeURIFilepath(rawURI, dirFilepath string, rootFilepaths []string) (string, error) {
// We use "file path" to mean a filesystem path.
uri, err := url.Parse(rawURI)
if err != nil {
return "", err
}
if uri.Scheme != "file" {
return "", fmt.Errorf("URI is not a file: %s", uri)
}
if uri.Path == "" {
// A more specific error than the one below, to catch the
// common mistake "file://foo".
return "", errors.New("empty path")
}
// The URI's path is interpreted relative to dirFilepath, and in the local filesystem.
// It must not try to escape its directory.
uriFilepathRel, err := filepath.Localize(strings.TrimPrefix(uri.Path, "/"))
if err != nil {
return "", fmt.Errorf("%q cannot be localized: %w", uriFilepathRel, err)
}
// Check roots, if there are any.
if len(rootFilepaths) > 0 {
// To check against the roots, we need an absolute file path, not relative to the directory.
// uriFilepath is local, so the joined path is under dirFilepath.
uriFilepathAbs := filepath.Join(dirFilepath, uriFilepathRel)
rootOK := false
// Check that the requested file path is under some root.
// Since both paths are absolute, that's equivalent to filepath.Rel constructing
// a local path.
for _, rootFilepathAbs := range rootFilepaths {
if rel, err := filepath.Rel(rootFilepathAbs, uriFilepathAbs); err == nil && filepath.IsLocal(rel) {
rootOK = true
break
}
}
if !rootOK {
return "", fmt.Errorf("URI path %q is not under any root", uriFilepathAbs)
}
}
return uriFilepathRel, nil
}
// withFile calls f on the file at join(dir, rel),
// protecting against path traversal attacks.
func withFile(dir, rel string, f func(*os.File) error) (err error) {
r, err := os.OpenRoot(dir)
if err != nil {
return err
}
defer r.Close()
file, err := r.Open(rel)
if err != nil {
return err
}
// Record error, in case f writes.
defer func() { err = errors.Join(err, file.Close()) }()
return f(file)
}
// fileRoots transforms the Roots obtained from the client into absolute paths on
// the local filesystem.
// TODO(jba): expose this functionality to user ResourceHandlers,
// so they don't have to repeat it.
func fileRoots(rawRoots []*Root) ([]string, error) {
var fileRoots []string
for _, r := range rawRoots {
fr, err := fileRoot(r)
if err != nil {
return nil, err
}
fileRoots = append(fileRoots, fr)
}
return fileRoots, nil
}
// fileRoot returns the absolute path for Root.
func fileRoot(root *Root) (_ string, err error) {
defer util.Wrapf(&err, "root %q", root.URI)
// Convert to absolute file path.
rurl, err := url.Parse(root.URI)
if err != nil {
return "", err
}
if rurl.Scheme != "file" {
return "", errors.New("not a file URI")
}
if rurl.Path == "" {
// A more specific error than the one below, to catch the
// common mistake "file://foo".
return "", errors.New("empty path")
}
// We don't want Localize here: we want an absolute path, which is not local.
fileRoot := filepath.Clean(filepath.FromSlash(rurl.Path))
if !filepath.IsAbs(fileRoot) {
return "", errors.New("not an absolute path")
}
return fileRoot, nil
}
// Matches reports whether the receiver's uri template matches the uri.
func (sr *serverResourceTemplate) Matches(uri string) bool {
tmpl, err := uritemplate.New(sr.resourceTemplate.URITemplate)
if err != nil {
return false
}
return tmpl.Regexp().MatchString(uri)
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"reflect"
"sync"
"github.com/google/jsonschema-go/jsonschema"
)
// A SchemaCache caches JSON schemas to avoid repeated reflection and resolution.
//
// This is useful for stateless server deployments (one [Server] per request)
// where tools are re-registered on every request. Without caching, each
// [AddTool] call triggers expensive reflection-based schema generation.
//
// A SchemaCache is safe for concurrent use by multiple goroutines.
//
// # Trade-offs
//
// The cache is unbounded: it stores one entry per unique Go type or schema
// pointer. For typical MCP servers with a fixed set of tools, memory usage
// is negligible. However, if tool input types are generated dynamically,
// the cache will grow without bound.
//
// The cache uses pointer identity for pre-defined schemas. If a schema's
// contents change but the pointer remains the same, stale resolved schemas
// may be returned. In practice, this is not an issue because tool schemas
// are typically defined once at startup.
type SchemaCache struct {
byType sync.Map // reflect.Type -> *cachedSchema
bySchema sync.Map // *jsonschema.Schema -> *jsonschema.Resolved
}
type cachedSchema struct {
schema *jsonschema.Schema
resolved *jsonschema.Resolved
}
// NewSchemaCache creates a new [SchemaCache].
func NewSchemaCache() *SchemaCache {
return &SchemaCache{}
}
func (c *SchemaCache) getByType(t reflect.Type) (*jsonschema.Schema, *jsonschema.Resolved, bool) {
if v, ok := c.byType.Load(t); ok {
cs := v.(*cachedSchema)
return cs.schema, cs.resolved, true
}
return nil, nil, false
}
func (c *SchemaCache) setByType(t reflect.Type, schema *jsonschema.Schema, resolved *jsonschema.Resolved) {
c.byType.Store(t, &cachedSchema{schema: schema, resolved: resolved})
}
func (c *SchemaCache) getBySchema(schema *jsonschema.Schema) (*jsonschema.Resolved, bool) {
if v, ok := c.bySchema.Load(schema); ok {
return v.(*jsonschema.Resolved), true
}
return nil, false
}
func (c *SchemaCache) setBySchema(schema *jsonschema.Schema, resolved *jsonschema.Resolved) {
c.bySchema.Store(schema, resolved)
}
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
// hasSessionID is the interface which, if implemented by connections, informs
// the session about their session ID.
//
// TODO(rfindley): remove SessionID methods from connections, when it doesn't
// make sense. Or remove it from the Sessions entirely: why does it even need
// to be exposed?
type hasSessionID interface {
SessionID() string
}
// ServerSessionState is the state of a session.
type ServerSessionState struct {
// InitializeParams are the parameters from 'initialize'.
InitializeParams *InitializeParams `json:"initializeParams"`
// InitializedParams are the parameters from 'notifications/initialized'.
InitializedParams *InitializedParams `json:"initializedParams"`
// LogLevel is the logging level for the session.
LogLevel LoggingLevel `json:"logLevel"`
// TODO: resource subscriptions
}
+611
View File
@@ -0,0 +1,611 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// This file contains code shared between client and server, including
// method handler and middleware definitions.
//
// Much of this is here so that we can factor out commonalities using
// generics. If this becomes unwieldy, it can perhaps be simplified with
// reflection.
package mcp
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"reflect"
"slices"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/auth"
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
)
const (
// latestProtocolVersion is the latest protocol version that this version of
// the SDK supports.
//
// It is the version that the client sends in the initialization request, and
// the default version used by the server.
latestProtocolVersion = protocolVersion20250618
protocolVersion20251125 = "2025-11-25" // not yet released
protocolVersion20250618 = "2025-06-18"
protocolVersion20250326 = "2025-03-26"
protocolVersion20241105 = "2024-11-05"
)
var supportedProtocolVersions = []string{
protocolVersion20251125,
protocolVersion20250618,
protocolVersion20250326,
protocolVersion20241105,
}
// negotiatedVersion returns the effective protocol version to use, given a
// client version.
func negotiatedVersion(clientVersion string) string {
// In general, prefer to use the clientVersion, but if we don't support the
// client's version, use the latest version.
//
// This handles the case where a new spec version is released, and the SDK
// does not support it yet.
if !slices.Contains(supportedProtocolVersions, clientVersion) {
return latestProtocolVersion
}
return clientVersion
}
// A MethodHandler handles MCP messages.
// For methods, exactly one of the return values must be nil.
// For notifications, both must be nil.
type MethodHandler func(ctx context.Context, method string, req Request) (result Result, err error)
// A Session is either a [ClientSession] or a [ServerSession].
type Session interface {
// ID returns the session ID, or the empty string if there is none.
ID() string
sendingMethodInfos() map[string]methodInfo
receivingMethodInfos() map[string]methodInfo
sendingMethodHandler() MethodHandler
receivingMethodHandler() MethodHandler
getConn() *jsonrpc2.Connection
}
// Middleware is a function from [MethodHandler] to [MethodHandler].
type Middleware func(MethodHandler) MethodHandler
// addMiddleware wraps the handler in the middleware functions.
func addMiddleware(handlerp *MethodHandler, middleware []Middleware) {
for _, m := range slices.Backward(middleware) {
*handlerp = m(*handlerp)
}
}
func defaultSendingMethodHandler(ctx context.Context, method string, req Request) (Result, error) {
info, ok := req.GetSession().sendingMethodInfos()[method]
if !ok {
// This can be called from user code, with an arbitrary value for method.
return nil, jsonrpc2.ErrNotHandled
}
params := req.GetParams()
if initParams, ok := params.(*InitializeParams); ok {
// Fix the marshaling of initialize params, to work around #607.
//
// The initialize params we produce should never be nil, nor have nil
// capabilities, so any panic here is a bug.
params = initParams.toV2()
}
// Notifications don't have results.
if strings.HasPrefix(method, "notifications/") {
return nil, req.GetSession().getConn().Notify(ctx, method, params)
}
// Create the result to unmarshal into.
// The concrete type of the result is the return type of the receiving function.
res := info.newResult()
if err := call(ctx, req.GetSession().getConn(), method, params, res); err != nil {
return nil, err
}
return res, nil
}
// Helper method to avoid typed nil.
func orZero[T any, P *U, U any](p P) T {
if p == nil {
var zero T
return zero
}
return any(p).(T)
}
func handleNotify(ctx context.Context, method string, req Request) error {
mh := req.GetSession().sendingMethodHandler()
_, err := mh(ctx, method, req)
return err
}
func handleSend[R Result](ctx context.Context, method string, req Request) (R, error) {
mh := req.GetSession().sendingMethodHandler()
// mh might be user code, so ensure that it returns the right values for the jsonrpc2 protocol.
res, err := mh(ctx, method, req)
if err != nil {
var z R
return z, err
}
return res.(R), nil
}
// defaultReceivingMethodHandler is the initial MethodHandler for servers and clients, before being wrapped by middleware.
func defaultReceivingMethodHandler[S Session](ctx context.Context, method string, req Request) (Result, error) {
info, ok := req.GetSession().receivingMethodInfos()[method]
if !ok {
// This can be called from user code, with an arbitrary value for method.
return nil, jsonrpc2.ErrNotHandled
}
return info.handleMethod(ctx, method, req)
}
func handleReceive[S Session](ctx context.Context, session S, jreq *jsonrpc.Request) (Result, error) {
info, err := checkRequest(jreq, session.receivingMethodInfos())
if err != nil {
return nil, err
}
params, err := info.unmarshalParams(jreq.Params)
if err != nil {
return nil, fmt.Errorf("handling '%s': %w", jreq.Method, err)
}
mh := session.receivingMethodHandler()
re, _ := jreq.Extra.(*RequestExtra)
req := info.newRequest(session, params, re)
// mh might be user code, so ensure that it returns the right values for the jsonrpc2 protocol.
res, err := mh(ctx, jreq.Method, req)
if err != nil {
return nil, err
}
return res, nil
}
// checkRequest checks the given request against the provided method info, to
// ensure it is a valid MCP request.
//
// If valid, the relevant method info is returned. Otherwise, a non-nil error
// is returned describing why the request is invalid.
//
// This is extracted from request handling so that it can be called in the
// transport layer to preemptively reject bad requests.
func checkRequest(req *jsonrpc.Request, infos map[string]methodInfo) (methodInfo, error) {
info, ok := infos[req.Method]
if !ok {
return methodInfo{}, fmt.Errorf("%w: %q unsupported", jsonrpc2.ErrNotHandled, req.Method)
}
if info.flags&notification != 0 && req.IsCall() {
return methodInfo{}, fmt.Errorf("%w: unexpected id for %q", jsonrpc2.ErrInvalidRequest, req.Method)
}
if info.flags&notification == 0 && !req.IsCall() {
return methodInfo{}, fmt.Errorf("%w: missing id for %q", jsonrpc2.ErrInvalidRequest, req.Method)
}
// missingParamsOK is checked here to catch the common case where "params" is
// missing entirely.
//
// However, it's checked again after unmarshalling to catch the rare but
// possible case where "params" is JSON null (see https://go.dev/issue/33835).
if info.flags&missingParamsOK == 0 && len(req.Params) == 0 {
return methodInfo{}, fmt.Errorf("%w: missing required \"params\"", jsonrpc2.ErrInvalidRequest)
}
return info, nil
}
// methodInfo is information about sending and receiving a method.
type methodInfo struct {
// flags is a collection of flags controlling how the JSONRPC method is
// handled. See individual flag values for documentation.
flags methodFlags
// Unmarshal params from the wire into a Params struct.
// Used on the receive side.
unmarshalParams func(json.RawMessage) (Params, error)
newRequest func(Session, Params, *RequestExtra) Request
// Run the code when a call to the method is received.
// Used on the receive side.
handleMethod MethodHandler
// Create a pointer to a Result struct.
// Used on the send side.
newResult func() Result
}
// The following definitions support converting from typed to untyped method handlers.
// Type parameter meanings:
// - S: sessions
// - P: params
// - R: results
// A typedMethodHandler is like a MethodHandler, but with type information.
type (
typedClientMethodHandler[P Params, R Result] func(context.Context, *ClientRequest[P]) (R, error)
typedServerMethodHandler[P Params, R Result] func(context.Context, *ServerRequest[P]) (R, error)
)
type paramsPtr[T any] interface {
*T
Params
}
type methodFlags int
const (
notification methodFlags = 1 << iota // method is a notification, not request
missingParamsOK // params may be missing or null
)
func newClientMethodInfo[P paramsPtr[T], R Result, T any](d typedClientMethodHandler[P, R], flags methodFlags) methodInfo {
mi := newMethodInfo[P, R](flags)
mi.newRequest = func(s Session, p Params, _ *RequestExtra) Request {
r := &ClientRequest[P]{Session: s.(*ClientSession)}
if p != nil {
r.Params = p.(P)
}
return r
}
mi.handleMethod = MethodHandler(func(ctx context.Context, _ string, req Request) (Result, error) {
return d(ctx, req.(*ClientRequest[P]))
})
return mi
}
func newServerMethodInfo[P paramsPtr[T], R Result, T any](d typedServerMethodHandler[P, R], flags methodFlags) methodInfo {
mi := newMethodInfo[P, R](flags)
mi.newRequest = func(s Session, p Params, re *RequestExtra) Request {
r := &ServerRequest[P]{Session: s.(*ServerSession), Extra: re}
if p != nil {
r.Params = p.(P)
}
return r
}
mi.handleMethod = MethodHandler(func(ctx context.Context, _ string, req Request) (Result, error) {
return d(ctx, req.(*ServerRequest[P]))
})
return mi
}
// newMethodInfo creates a methodInfo from a typedMethodHandler.
//
// If isRequest is set, the method is treated as a request rather than a
// notification.
func newMethodInfo[P paramsPtr[T], R Result, T any](flags methodFlags) methodInfo {
return methodInfo{
flags: flags,
unmarshalParams: func(m json.RawMessage) (Params, error) {
var p P
if m != nil {
if err := internaljson.Unmarshal(m, &p); err != nil {
return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, p, err)
}
}
// We must check missingParamsOK here, in addition to checkRequest, to
// catch the edge cases where "params" is set to JSON null.
// See also https://go.dev/issue/33835.
//
// We need to ensure that p is non-null to guard against crashes, as our
// internal code or externally provided handlers may assume that params
// is non-null.
if flags&missingParamsOK == 0 && p == nil {
return nil, fmt.Errorf("%w: missing required \"params\"", jsonrpc2.ErrInvalidRequest)
}
return orZero[Params](p), nil
},
// newResult is used on the send side, to construct the value to unmarshal the result into.
// R is a pointer to a result struct. There is no way to "unpointer" it without reflection.
// TODO(jba): explore generic approaches to this, perhaps by treating R in
// the signature as the unpointered type.
newResult: func() Result { return reflect.New(reflect.TypeFor[R]().Elem()).Interface().(R) },
}
}
// serverMethod is glue for creating a typedMethodHandler from a method on Server.
func serverMethod[P Params, R Result](
f func(*Server, context.Context, *ServerRequest[P]) (R, error),
) typedServerMethodHandler[P, R] {
return func(ctx context.Context, req *ServerRequest[P]) (R, error) {
return f(req.Session.server, ctx, req)
}
}
// clientMethod is glue for creating a typedMethodHandler from a method on Client.
func clientMethod[P Params, R Result](
f func(*Client, context.Context, *ClientRequest[P]) (R, error),
) typedClientMethodHandler[P, R] {
return func(ctx context.Context, req *ClientRequest[P]) (R, error) {
return f(req.Session.client, ctx, req)
}
}
// serverSessionMethod is glue for creating a typedServerMethodHandler from a method on ServerSession.
func serverSessionMethod[P Params, R Result](f func(*ServerSession, context.Context, P) (R, error)) typedServerMethodHandler[P, R] {
return func(ctx context.Context, req *ServerRequest[P]) (R, error) {
return f(req.GetSession().(*ServerSession), ctx, req.Params)
}
}
// clientSessionMethod is glue for creating a typedMethodHandler from a method on ServerSession.
func clientSessionMethod[P Params, R Result](f func(*ClientSession, context.Context, P) (R, error)) typedClientMethodHandler[P, R] {
return func(ctx context.Context, req *ClientRequest[P]) (R, error) {
return f(req.GetSession().(*ClientSession), ctx, req.Params)
}
}
// MCP-specific error codes.
const (
// CodeResourceNotFound indicates that a requested resource could not be found.
CodeResourceNotFound = -32002
// CodeURLElicitationRequired indicates that the server requires URL elicitation
// before processing the request. The client should execute the elicitation handler
// with the elicitations provided in the error data.
CodeURLElicitationRequired = -32042
)
// URLElicitationRequiredError returns an error indicating that URL elicitation is required
// before the request can be processed. The elicitations parameter should contain the
// elicitation requests that must be completed.
func URLElicitationRequiredError(elicitations []*ElicitParams) error {
// Validate that all elicitations are URL mode
for _, elicit := range elicitations {
mode := elicit.Mode
if mode == "" {
mode = "form" // default mode
}
if mode != "url" {
panic(fmt.Sprintf("URLElicitationRequiredError requires all elicitations to be URL mode, got %q", mode))
}
}
data, err := json.Marshal(map[string]any{
"elicitations": elicitations,
})
if err != nil {
// This should never happen with valid ElicitParams
panic(fmt.Sprintf("failed to marshal elicitations: %v", err))
}
return &jsonrpc.Error{
Code: CodeURLElicitationRequired,
Message: "URL elicitation required",
Data: json.RawMessage(data),
}
}
// Internal error codes
const (
// The error code if the method exists and was called properly, but the peer does not support it.
//
// TODO(rfindley): this code is wrong, and we should fix it to be
// consistent with other SDKs.
codeUnsupportedMethod = -31001
)
// notifySessions calls Notify on all the sessions.
// Should be called on a copy of the peer sessions.
// The logger must be non-nil.
func notifySessions[S Session, P Params](sessions []S, method string, params P, logger *slog.Logger) {
if sessions == nil {
return
}
// Notify with the background context, so the messages are sent on the
// standalone stream.
// TODO: make this timeout configurable, or call handleNotify asynchronously.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// TODO: there's a potential spec violation here, when the feature list
// changes before the session (client or server) is initialized.
for _, s := range sessions {
req := newRequest(s, params)
if err := handleNotify(ctx, method, req); err != nil {
logger.Warn(fmt.Sprintf("calling %s: %v", method, err))
}
}
}
func newRequest[S Session, P Params](s S, p P) Request {
switch s := any(s).(type) {
case *ClientSession:
return &ClientRequest[P]{Session: s, Params: p}
case *ServerSession:
return &ServerRequest[P]{Session: s, Params: p}
default:
panic("bad session")
}
}
// Meta is additional metadata for requests, responses and other types.
type Meta map[string]any
// GetMeta returns metadata from a value.
func (m Meta) GetMeta() map[string]any { return m }
// SetMeta sets the metadata on a value.
func (m *Meta) SetMeta(x map[string]any) { *m = x }
const progressTokenKey = "progressToken"
func getProgressToken(p Params) any {
return p.GetMeta()[progressTokenKey]
}
func setProgressToken(p Params, pt any) {
switch pt.(type) {
// Support int32 and int64 for atomic.IntNN.
case int, int32, int64, string:
default:
panic(fmt.Sprintf("progress token %v is of type %[1]T, not int or string", pt))
}
m := p.GetMeta()
if m == nil {
m = map[string]any{}
}
m[progressTokenKey] = pt
}
// A Request is a method request with parameters and additional information, such as the session.
// Request is implemented by [*ClientRequest] and [*ServerRequest].
type Request interface {
isRequest()
GetSession() Session
GetParams() Params
// GetExtra returns the Extra field for ServerRequests, and nil for ClientRequests.
GetExtra() *RequestExtra
}
// A ClientRequest is a request to a client.
type ClientRequest[P Params] struct {
Session *ClientSession
Params P
}
// A ServerRequest is a request to a server.
type ServerRequest[P Params] struct {
Session *ServerSession
Params P
Extra *RequestExtra
}
// RequestExtra is extra information included in requests, typically from
// the transport layer.
type RequestExtra struct {
TokenInfo *auth.TokenInfo // bearer token info (e.g. from OAuth) if any
Header http.Header // header from HTTP request, if any
// If set, CloseSSEStream explicitly closes the current SSE request stream.
//
// [SEP-1699] introduced server-side SSE stream disconnection: for
// long-running requests, servers may opt to close the SSE stream and
// ask the client to retry at a later time. CloseSSEStream implements this
// feature; if RetryAfter is set, an event is sent with a `retry:` field
// to configure the reconnection delay.
//
// [SEP-1699]: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699
CloseSSEStream func(CloseSSEStreamArgs)
}
// CloseSSEStreamArgs are arguments for [RequestExtra.CloseSSEStream].
type CloseSSEStreamArgs struct {
// RetryAfter configures the reconnection delay sent to the client via the
// SSE retry field. If zero, no retry field is sent.
RetryAfter time.Duration
}
func (*ClientRequest[P]) isRequest() {}
func (*ServerRequest[P]) isRequest() {}
func (r *ClientRequest[P]) GetSession() Session { return r.Session }
func (r *ServerRequest[P]) GetSession() Session { return r.Session }
func (r *ClientRequest[P]) GetParams() Params { return r.Params }
func (r *ServerRequest[P]) GetParams() Params { return r.Params }
func (r *ClientRequest[P]) GetExtra() *RequestExtra { return nil }
func (r *ServerRequest[P]) GetExtra() *RequestExtra { return r.Extra }
func serverRequestFor[P Params](s *ServerSession, p P) *ServerRequest[P] {
return &ServerRequest[P]{Session: s, Params: p}
}
func clientRequestFor[P Params](s *ClientSession, p P) *ClientRequest[P] {
return &ClientRequest[P]{Session: s, Params: p}
}
// Params is a parameter (input) type for an MCP call or notification.
type Params interface {
// GetMeta returns metadata from a value.
GetMeta() map[string]any
// SetMeta sets the metadata on a value.
SetMeta(map[string]any)
// isParams discourages implementation of Params outside of this package.
isParams()
}
// RequestParams is a parameter (input) type for an MCP request.
type RequestParams interface {
Params
// GetProgressToken returns the progress token from the params' Meta field, or nil
// if there is none.
GetProgressToken() any
// SetProgressToken sets the given progress token into the params' Meta field.
// It panics if its argument is not an int or a string.
SetProgressToken(any)
}
// Result is a result of an MCP call.
type Result interface {
// isResult discourages implementation of Result outside of this package.
isResult()
// GetMeta returns metadata from a value.
GetMeta() map[string]any
// SetMeta sets the metadata on a value.
SetMeta(map[string]any)
}
// emptyResult is returned by methods that have no result, like ping.
// Those methods cannot return nil, because jsonrpc2 cannot handle nils.
type emptyResult struct{}
func (*emptyResult) isResult() {}
func (*emptyResult) GetMeta() map[string]any { panic("should never be called") }
func (*emptyResult) SetMeta(map[string]any) { panic("should never be called") }
type listParams interface {
// Returns a pointer to the param's Cursor field.
cursorPtr() *string
}
type listResult[T any] interface {
// Returns a pointer to the param's NextCursor field.
nextCursorPtr() *string
}
// keepaliveSession represents a session that supports keepalive functionality.
type keepaliveSession interface {
Ping(ctx context.Context, params *PingParams) error
Close() error
}
// startKeepalive starts the keepalive mechanism for a session.
// It assigns the cancel function to the provided cancelPtr and starts a goroutine
// that sends ping messages at the specified interval.
func startKeepalive(session keepaliveSession, interval time.Duration, cancelPtr *context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
// Assign cancel function before starting goroutine to avoid race condition.
// We cannot return it because the caller may need to cancel during the
// window between goroutine scheduling and function return.
*cancelPtr = cancel
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
pingCtx, pingCancel := context.WithTimeout(context.Background(), interval/2)
err := session.Ping(pingCtx, nil)
pingCancel()
if err != nil {
// Ping failed, close the session
_ = session.Close()
return
}
}
}
}()
}
+489
View File
@@ -0,0 +1,489 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
)
// This file implements support for SSE (HTTP with server-sent events)
// transport server and client.
// https://modelcontextprotocol.io/specification/2024-11-05/basic/transports
//
// The transport is simple, at least relative to the new streamable transport
// introduced in the 2025-03-26 version of the spec. In short:
//
// 1. Sessions are initiated via a hanging GET request, which streams
// server->client messages as SSE 'message' events.
// 2. The first event in the SSE stream must be an 'endpoint' event that
// informs the client of the session endpoint.
// 3. The client POSTs client->server messages to the session endpoint.
//
// Therefore, the each new GET request hands off its responsewriter to an
// [SSEServerTransport] type that abstracts the transport as follows:
// - Write writes a new event to the responseWriter, or fails if the GET has
// exited.
// - Read reads off a message queue that is pushed to via POST requests.
// - Close causes the hanging GET to exit.
// SSEHandler is an http.Handler that serves SSE-based MCP sessions as defined by
// the [2024-11-05 version] of the MCP spec.
//
// [2024-11-05 version]: https://modelcontextprotocol.io/specification/2024-11-05/basic/transports
type SSEHandler struct {
getServer func(request *http.Request) *Server
opts SSEOptions
onConnection func(*ServerSession) // for testing; must not block
mu sync.Mutex
sessions map[string]*SSEServerTransport
}
// SSEOptions specifies options for an [SSEHandler].
// for now, it is empty, but may be extended in future.
// https://github.com/modelcontextprotocol/go-sdk/issues/507
type SSEOptions struct{}
// NewSSEHandler returns a new [SSEHandler] that creates and manages MCP
// sessions created via incoming HTTP requests.
//
// Sessions are created when the client issues a GET request to the server,
// which must accept text/event-stream responses (server-sent events).
// For each such request, a new [SSEServerTransport] is created with a distinct
// messages endpoint, and connected to the server returned by getServer.
// The SSEHandler also handles requests to the message endpoints, by
// delegating them to the relevant server transport.
//
// The getServer function may return a distinct [Server] for each new
// request, or reuse an existing server. If it returns nil, the handler
// will return a 400 Bad Request.
func NewSSEHandler(getServer func(request *http.Request) *Server, opts *SSEOptions) *SSEHandler {
s := &SSEHandler{
getServer: getServer,
sessions: make(map[string]*SSEServerTransport),
}
if opts != nil {
s.opts = *opts
}
return s
}
// A SSEServerTransport is a logical SSE session created through a hanging GET
// request.
//
// Use [SSEServerTransport.Connect] to initiate the flow of messages.
//
// When connected, it returns the following [Connection] implementation:
// - Writes are SSE 'message' events to the GET response.
// - Reads are received from POSTs to the session endpoint, via
// [SSEServerTransport.ServeHTTP].
// - Close terminates the hanging GET.
//
// The transport is itself an [http.Handler]. It is the caller's responsibility
// to ensure that the resulting transport serves HTTP requests on the given
// session endpoint.
//
// Each SSEServerTransport may be connected (via [Server.Connect]) at most
// once, since [SSEServerTransport.ServeHTTP] serves messages to the connected
// session.
//
// Most callers should instead use an [SSEHandler], which transparently handles
// the delegation to SSEServerTransports.
type SSEServerTransport struct {
// Endpoint is the endpoint for this session, where the client can POST
// messages.
Endpoint string
// Response is the hanging response body to the incoming GET request.
Response http.ResponseWriter
// incoming is the queue of incoming messages.
// It is never closed, and by convention, incoming is non-nil if and only if
// the transport is connected.
incoming chan jsonrpc.Message
// We must guard both pushes to the incoming queue and writes to the response
// writer, because incoming POST requests are arbitrarily concurrent and we
// need to ensure we don't write push to the queue, or write to the
// ResponseWriter, after the session GET request exits.
mu sync.Mutex // also guards writes to Response
closed bool // set when the stream is closed
done chan struct{} // closed when the connection is closed
}
// ServeHTTP handles POST requests to the transport endpoint.
func (t *SSEServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if t.incoming == nil {
http.Error(w, "session not connected", http.StatusInternalServerError)
return
}
// Read and parse the message.
data, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
// Optionally, we could just push the data onto a channel, and let the
// message fail to parse when it is read. This failure seems a bit more
// useful
msg, err := jsonrpc2.DecodeMessage(data)
if err != nil {
http.Error(w, "failed to parse body", http.StatusBadRequest)
return
}
if req, ok := msg.(*jsonrpc.Request); ok {
if _, err := checkRequest(req, serverMethodInfos); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
select {
case t.incoming <- msg:
w.WriteHeader(http.StatusAccepted)
case <-t.done:
http.Error(w, "session closed", http.StatusBadRequest)
}
}
// Connect sends the 'endpoint' event to the client.
// See [SSEServerTransport] for more details on the [Connection] implementation.
func (t *SSEServerTransport) Connect(context.Context) (Connection, error) {
if t.incoming != nil {
return nil, fmt.Errorf("already connected")
}
t.incoming = make(chan jsonrpc.Message, 100)
t.done = make(chan struct{})
_, err := writeEvent(t.Response, Event{
Name: "endpoint",
Data: []byte(t.Endpoint),
})
if err != nil {
return nil, err
}
return &sseServerConn{t: t}, nil
}
func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
sessionID := req.URL.Query().Get("sessionid")
// TODO: consider checking Content-Type here. For now, we are lax.
// For POST requests, the message body is a message to send to a session.
if req.Method == http.MethodPost {
// Look up the session.
if sessionID == "" {
http.Error(w, "sessionid must be provided", http.StatusBadRequest)
return
}
h.mu.Lock()
session := h.sessions[sessionID]
h.mu.Unlock()
if session == nil {
http.Error(w, "session not found", http.StatusNotFound)
return
}
session.ServeHTTP(w, req)
return
}
if req.Method != http.MethodGet {
w.Header().Set("Allow", "GET, POST")
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// GET requests create a new session, and serve messages over SSE.
// TODO: it's not entirely documented whether we should check Accept here.
// Let's again be lax and assume the client will accept SSE.
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
sessionID = rand.Text()
endpoint, err := req.URL.Parse("?sessionid=" + sessionID)
if err != nil {
http.Error(w, "internal error: failed to create endpoint", http.StatusInternalServerError)
return
}
transport := &SSEServerTransport{Endpoint: endpoint.RequestURI(), Response: w}
// The session is terminated when the request exits.
h.mu.Lock()
h.sessions[sessionID] = transport
h.mu.Unlock()
defer func() {
h.mu.Lock()
delete(h.sessions, sessionID)
h.mu.Unlock()
}()
server := h.getServer(req)
if server == nil {
// The getServer argument to NewSSEHandler returned nil.
http.Error(w, "no server available", http.StatusBadRequest)
return
}
ss, err := server.Connect(req.Context(), transport, nil)
if err != nil {
http.Error(w, "connection failed", http.StatusInternalServerError)
return
}
if h.onConnection != nil {
h.onConnection(ss)
}
defer ss.Close() // close the transport when the GET exits
select {
case <-req.Context().Done():
case <-transport.done:
}
}
// sseServerConn implements the [Connection] interface for a single [SSEServerTransport].
// It hides the Connection interface from the SSEServerTransport API.
type sseServerConn struct {
t *SSEServerTransport
}
// TODO(jba): get the session ID. (Not urgent because SSE transports have been removed from the spec.)
func (s *sseServerConn) SessionID() string { return "" }
// Read implements jsonrpc2.Reader.
func (s *sseServerConn) Read(ctx context.Context) (jsonrpc.Message, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case msg := <-s.t.incoming:
return msg, nil
case <-s.t.done:
return nil, io.EOF
}
}
// Write implements jsonrpc2.Writer.
func (s *sseServerConn) Write(ctx context.Context, msg jsonrpc.Message) error {
if ctx.Err() != nil {
return ctx.Err()
}
data, err := jsonrpc2.EncodeMessage(msg)
if err != nil {
return err
}
s.t.mu.Lock()
defer s.t.mu.Unlock()
// Note that it is invalid to write to a ResponseWriter after ServeHTTP has
// exited, and so we must lock around this write and check isDone, which is
// set before the hanging GET exits.
if s.t.closed {
return io.EOF
}
_, err = writeEvent(s.t.Response, Event{Name: "message", Data: data})
return err
}
// Close implements io.Closer, and closes the session.
//
// It must be safe to call Close more than once, as the close may
// asynchronously be initiated by either the server closing its connection, or
// by the hanging GET exiting.
func (s *sseServerConn) Close() error {
s.t.mu.Lock()
defer s.t.mu.Unlock()
if !s.t.closed {
s.t.closed = true
close(s.t.done)
}
return nil
}
// An SSEClientTransport is a [Transport] that can communicate with an MCP
// endpoint serving the SSE transport defined by the 2024-11-05 version of the
// spec.
//
// https://modelcontextprotocol.io/specification/2024-11-05/basic/transports
type SSEClientTransport struct {
// Endpoint is the SSE endpoint to connect to.
Endpoint string
// HTTPClient is the client to use for making HTTP requests. If nil,
// http.DefaultClient is used.
HTTPClient *http.Client
}
// Connect connects through the client endpoint.
func (c *SSEClientTransport) Connect(ctx context.Context) (Connection, error) {
parsedURL, err := url.Parse(c.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid endpoint: %v", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", c.Endpoint, nil)
if err != nil {
return nil, err
}
httpClient := c.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
req.Header.Set("Accept", "text/event-stream")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
// Check HTTP status code before attempting to parse SSE events.
// This ensures proper error reporting for authentication failures (401),
// authorization failures (403), and other HTTP errors.
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
resp.Body.Close()
return nil, fmt.Errorf("failed to connect: %s", http.StatusText(resp.StatusCode))
}
msgEndpoint, err := func() (*url.URL, error) {
var evt Event
for evt, err = range scanEvents(resp.Body) {
break
}
if err != nil {
return nil, err
}
if evt.Name != "endpoint" {
return nil, fmt.Errorf("first event is %q, want %q", evt.Name, "endpoint")
}
raw := string(evt.Data)
return parsedURL.Parse(raw)
}()
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("missing endpoint: %v", err)
}
// From here on, the stream takes ownership of resp.Body.
s := &sseClientConn{
client: httpClient,
msgEndpoint: msgEndpoint,
incoming: make(chan []byte, 100),
body: resp.Body,
done: make(chan struct{}),
}
go func() {
defer s.Close() // close the transport when the GET exits
for evt, err := range scanEvents(resp.Body) {
if err != nil {
return
}
select {
case s.incoming <- evt.Data:
case <-s.done:
return
}
}
}()
return s, nil
}
// An sseClientConn is a logical jsonrpc2 connection that implements the client
// half of the SSE protocol:
// - Writes are POSTS to the session endpoint.
// - Reads are SSE 'message' events, and pushes them onto a buffered channel.
// - Close terminates the GET request.
type sseClientConn struct {
client *http.Client // HTTP client to use for requests
msgEndpoint *url.URL // session endpoint for POSTs
incoming chan []byte // queue of incoming messages
mu sync.Mutex
body io.ReadCloser // body of the hanging GET
closed bool // set when the stream is closed
done chan struct{} // closed when the stream is closed
}
// TODO(jba): get the session ID. (Not urgent because SSE transports have been removed from the spec.)
func (c *sseClientConn) SessionID() string { return "" }
func (c *sseClientConn) isDone() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.closed
}
func (c *sseClientConn) Read(ctx context.Context) (jsonrpc.Message, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-c.done:
return nil, io.EOF
case data := <-c.incoming:
// TODO(rfindley): do we really need to check this? We receive from c.done above.
if c.isDone() {
return nil, io.EOF
}
msg, err := jsonrpc2.DecodeMessage(data)
if err != nil {
return nil, err
}
return msg, nil
}
}
func (c *sseClientConn) Write(ctx context.Context, msg jsonrpc.Message) error {
data, err := jsonrpc2.EncodeMessage(msg)
if err != nil {
return err
}
if c.isDone() {
return io.EOF
}
req, err := http.NewRequestWithContext(ctx, "POST", c.msgEndpoint.String(), bytes.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("failed to write: %s", resp.Status)
}
return nil
}
func (c *sseClientConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.closed {
c.closed = true
_ = c.body.Close()
close(c.done)
}
return nil
}
File diff suppressed because it is too large Load Diff
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// TODO: move client-side streamable HTTP logic from streamable.go to this file.
package mcp
/*
Streamable HTTP Client Design
This document describes the client-side implementation of the MCP streamable
HTTP transport, as defined by the MCP spec:
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
# Overview
The client-side streamable transport allows an MCP client to communicate with a
server over HTTP, sending messages via POST and receiving responses via either
JSON or server-sent events (SSE). The implementation consists of two main
components:
┌─────────────────────────────────────────────────────────────────┐
│ [StreamableClientTransport] │
│ Transport configuration; creates connections via Connect() │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ [streamableClientConn] │
│ Connection implementation; handles HTTP request/response │
└─────────────────────────────────────────────────────────────────┘
├──────────────────────────────────────┐
▼ ▼
┌─────────────────────────────────────────┐ ┌────────────────────────────────────┐
│ POST request handlers │ │ Standalone SSE stream │
│ (one per outgoing message/call) │ │ (server-initiated messages) │
└─────────────────────────────────────────┘ └────────────────────────────────────┘
# Sessions
The client maintains a session with the server, identified by a session ID
(Mcp-Session-Id header):
- Session ID is received from the server after initialization
- Client includes the session ID in all subsequent requests
- Session ends when the client calls Close() (sends DELETE) or server returns 404
[streamableClientConn] stores the session state:
- [streamableClientConn.sessionID]: Server-assigned session identifier
- [streamableClientConn.initializedResult]: Protocol version and server capabilities
# Connection Lifecycle
1. Connect: [StreamableClientTransport.Connect] creates a [streamableClientConn]
with a detached context for the connection's lifetime. The context is detached
to prevent the standalone SSE stream from being cancelled when the original
Connect context times out.
2. Initialize: The MCP client sends initialize/initialized messages. Upon
receiving [InitializeResult], the connection:
- Stores the negotiated protocol version for the Mcp-Protocol-Version header
- Captures the session ID from the Mcp-Session-Id response header
- Starts the standalone SSE stream via [streamableClientConn.connectStandaloneSSE]
3. Operation: Messages are sent via POST, responses received via JSON or SSE.
4. Close: [streamableClientConn.Close] sends a DELETE request to terminate
the session (unless the session is already gone), then cancels the connection
context to clean up the standalone SSE stream.
# Sending Messages (Write)
[streamableClientConn.Write] sends all outgoing messages via HTTP POST:
POST /endpoint
Content-Type: application/json
Accept: application/json, text/event-stream
Mcp-Protocol-Version: <negotiated version>
Mcp-Session-Id: <session ID, if established>
<JSON-RPC message>
The server may respond with:
- 202 Accepted: Message received, no response body (notifications/responses)
- 200 OK with application/json: Single JSON-RPC response
- 200 OK with text/event-stream: SSE stream of responses
# Receiving Messages (Read)
[streamableClientConn.Read] returns messages from the [streamableClientConn.incoming]
channel, which is populated by multiple concurrent goroutines:
1. POST response handlers ([streamableClientConn.handleJSON] and
[streamableClientConn.handleSSE]): Process responses from POST requests
2. Standalone SSE stream: Receives server-initiated requests and notifications
The client handles both response formats:
- JSON: [streamableClientConn.handleJSON] reads body, decodes message
- SSE: [streamableClientConn.handleSSE] scans events, decodes each message
# Standalone SSE Stream
After initialization, [streamableClientConn.sessionUpdated] triggers
[streamableClientConn.connectStandaloneSSE] to open a GET request for
server-initiated messages:
GET /endpoint
Accept: text/event-stream
Mcp-Session-Id: <session ID>
Stream behavior:
- Optional: Server may return 405 Method Not Allowed (spec-compliant) or
other 4xx errors (tolerated in non-strict mode for compatibility)
- Persistent: Runs for the connection lifetime in a background goroutine
- Resumable: Uses Last-Event-ID header on reconnection if server provides event IDs
- Reconnects: Automatic reconnection with exponential backoff on interruption
# Stream Resumption
When an SSE stream (standalone or POST response) is interrupted, the client
attempts to reconnect using [streamableClientConn.connectSSE]:
Event ID tracking:
- [streamableClientConn.processStream] tracks the last received event ID
- On reconnection, the Last-Event-ID header is set to resume from that point
- Server replays missed events if it has an [EventStore] configured
See [calculateReconnectDelay] for the reconnect delay details.
Server-initiated reconnection (SEP-1699)
- SSE retry field: Sets the delay for the next reconnect attempt
- If server doesn't provide event IDs, non-standalone streams don't reconnect
# Response Formats
The client must handle two response formats from POST requests:
1. application/json: Single JSON-RPC response
- Body contains one JSON-RPC message
- Handled by [streamableClientConn.handleJSON]
- Simpler but doesn't support streaming or server-initiated messages
2. text/event-stream: SSE stream of messages
- Body contains SSE events with JSON-RPC messages
- Handled by [streamableClientConn.handleSSE]
- Supports multiple messages and server-initiated communication
- Stream completes when the response to the originating call is received
# HTTP Methods
- POST: Send JSON-RPC messages (requests, responses, notifications)
- Used by [streamableClientConn.Write]
- Response may be JSON or SSE
- GET: Open or resume SSE stream for server-initiated messages
- Used by [streamableClientConn.connectSSE]
- Always expects text/event-stream response (or 405)
- DELETE: Terminate the session
- Used by [streamableClientConn.Close]
- Skipped if session is already known to be gone ([ErrSessionMissing])
# Error Handling
Errors are categorized and handled differently:
1. Transient (recoverable via reconnection):
- Network interruption during SSE streaming
- Connection reset or timeout
- Triggers reconnection in [streamableClientConn.handleSSE]
2. Terminal (breaks the connection):
- 404 Not Found: Session terminated by server ([ErrSessionMissing])
- Message decode errors: Protocol violation
- Context cancellation: Client closed connection
- Mismatched session IDs: Protocol error
- See issue #683: our terminal errors are too strict.
Terminal errors are stored via [streamableClientConn.fail] and returned by
subsequent [streamableClientConn.Read] calls. The [streamableClientConn.failed]
channel signals that the connection is broken.
Special case: [ErrSessionMissing] indicates the server has terminated the session,
so [streamableClientConn.Close] skips the DELETE request.
# Protocol Version Header
After initialization, all requests include:
Mcp-Protocol-Version: <negotiated version>
This header (set by [streamableClientConn.setMCPHeaders]):
- Allows the server to handle requests per the negotiated protocol
- Is omitted before initialization completes
- Uses the version from [streamableClientConn.initializedResult]
# Key Implementation Details
[StreamableClientTransport] configuration:
- [StreamableClientTransport.Endpoint]: URL of the MCP server
- [StreamableClientTransport.HTTPClient]: Custom HTTP client (optional)
- [StreamableClientTransport.MaxRetries]: Reconnection attempts (default 5)
[streamableClientConn] handles the [Connection] interface:
- [streamableClientConn.Read]: Returns messages from incoming channel
- [streamableClientConn.Write]: Sends messages via POST, starts response handlers
- [streamableClientConn.Close]: Sends DELETE, cancels context, closes done channel
State management:
- [streamableClientConn.incoming]: Buffered channel for received messages
- [streamableClientConn.sessionID]: Server-assigned session identifier
- [streamableClientConn.initializedResult]: Cached for protocol version header
- [streamableClientConn.failed]: Channel closed on terminal error
- [streamableClientConn.done]: Channel closed on graceful shutdown
- [streamableClientConn.ctx]: Detached context for connection lifetime
- [streamableClientConn.cancel]: Cancels ctx to terminate SSE streams
Context handling:
- Connection context is detached from [StreamableClientTransport.Connect] context
using [xcontext.Detach] to preserve context values (for auth middleware) while
preventing premature cancellation of the standalone SSE stream
- Individual POST requests use caller-provided contexts for cancellation
*/
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// TODO: move server-side streamable HTTP logic from streamable.go to this file.
package mcp
/*
Streamable HTTP Server Design
This document describes the server-side implementation of the MCP streamable
HTTP transport, as defined by the MCP spec:
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
# Overview
The streamable HTTP transport enables MCP communication over HTTP, with
server-sent events (SSE) for server-to-client messages. The implementation
consists of several layered components:
┌─────────────────────────────────────────────────────────────────┐
│ [StreamableHTTPHandler] │
│ http.Handler that manages sessions and routes HTTP requests │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ [StreamableServerTransport] │
│ transport implementation, one per session; exposes ServeHTTP │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ [streamableServerConn] │
│ Connection implementation, handles message routing │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ [stream] │
│ Logical message channel within a session, may be resumed │
└─────────────────────────────────────────────────────────────────┘
# Sessions
As with other transports, a session represents a logical MCP connection between
a client and server. In the streamable transport, sessions are identified by a
unique session ID (Mcp-Session-Id header) and persist across multiple HTTP
requests.
[StreamableHTTPHandler] maintains a map of active sessions ([sessionInfo]),
each containing:
- The [ServerSession] (MCP-level session state)
- The [StreamableServerTransport] (for message I/O)
- Optional timeout management for idle session cleanup
Sessions are created on the first POST request (typically containing the
initialize request) and destroyed either by:
- Client sending a DELETE request
- Session timeout due to inactivity
- Server explicitly closing the session
# Streams
Within a session, there can be multiple concurrent "streams" - logical channels
for message delivery. This is distinct from HTTP streams; a single [stream] may
span multiple HTTP request/response cycles (via resumption).
There are two types of streams:
1. Optional standalone SSE stream (id = ""):
- Created when client sends a GET request to the endpoint
- Used for server-initiated messages (requests/notifications to client)
- Persists for the lifetime of the session
- Only one standalone stream per session
2. Request streams (id = random string):
- Created for each POST request containing JSON-RPC calls
- Used to route responses back to the originating HTTP request
- Completed when all responses have been sent
- Can be resumed via GET with Last-Event-ID if interrupted
# Message Routing
When the server writes a message, it must be routed to the correct [stream]:
- Responses: Routed to the stream that originated the request
- Requests/Notifications made during request handling: Routed to the same
stream as the triggering request (via context)
- Requests/Notifications made outside request handling: Routed to the
standalone SSE stream
This routing is implemented using:
- [streamableServerConn.requestStreams] maps request IDs to stream IDs
- [idContextKey] is used to store the originating request ID in Context
- [streamableServerConn.streams] maps stream IDs to [stream] objects
# Stream Resumption
If an HTTP connection is interrupted (network issues, etc.), clients can
resume a stream by sending a GET request with the Last-Event-ID header.
This requires an [EventStore] to be configured on the server.
- [EventStore.Open] is called when a new stream is created
- [EventStore.Append] is called for each message written to the stream
- [EventStore.After] is called to replay messages after a given index
- [EventStore.SessionClosed] is called when the session ends
Event IDs are formatted as "<streamID>_<index>" to identify both the
stream and position within that stream (see [formatEventID] and [parseEventID]).
# Stateless Mode
For simpler deployments, the handler supports "stateless" mode
([StreamableHTTPOptions.Stateless]) where:
- No session ID validation is performed
- Each request creates a temporary session that's closed after the request
- Server-to-client requests are not supported (no way to receive response)
This mode is useful for simple tool servers that don't need bidirectional
communication.
# Response Formats
The server can respond to POST requests in two formats:
1. text/event-stream (default): Messages sent as SSE events, supports
streaming multiple messages and server-initiated communication during
request handling.
2. application/json ([StreamableHTTPOptions.JSONResponse]): Single JSON
response, simpler but doesn't support streaming. Server-initiated messages
during request handling go to the standalone SSE stream instead.
# HTTP Methods
- POST: Send JSON-RPC messages (requests, responses, notifications)
- GET: Open standalone SSE stream or resume an interrupted stream
- DELETE: Terminate the session
# Key Implementation Details
The [stream] struct manages delivery of messages to HTTP responses.
Fields:
- [stream.w] is the ResponseWriter for the current HTTP response (non-nil indicates claimed)
- [stream.done] is closed to release the hanging HTTP request
- [stream.requests] tracks pending request IDs (stream completes when empty)
Methods:
- [stream.deliverLocked] delivers a message to the stream
- [stream.close] sends a close event and releases the stream
- [stream.release] releases the stream from the HTTP request, allowing resumption
[streamableServerConn] handles the [Connection] interface:
- [streamableServerConn.Read] receives messages from the incoming channel (fed by POST handlers)
- [streamableServerConn.Write] routes messages to appropriate streams
- [streamableServerConn.Close] terminates the session and notifies the [EventStore]
*/
+140
View File
@@ -0,0 +1,140 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/google/jsonschema-go/jsonschema"
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
)
// A ToolHandler handles a call to tools/call.
//
// This is a low-level API, for use with [Server.AddTool]. It does not do any
// pre- or post-processing of the request or result: the params contain raw
// arguments, no input validation is performed, and the result is returned to
// the user as-is, without any validation of the output.
//
// Most users will write a [ToolHandlerFor] and install it with the generic
// [AddTool] function.
//
// If ToolHandler returns an error, it is treated as a protocol error. By
// contrast, [ToolHandlerFor] automatically populates [CallToolResult.IsError]
// and [CallToolResult.Content] accordingly.
type ToolHandler func(context.Context, *CallToolRequest) (*CallToolResult, error)
// A ToolHandlerFor handles a call to tools/call with typed arguments and results.
//
// Use [AddTool] to add a ToolHandlerFor to a server.
//
// Unlike [ToolHandler], [ToolHandlerFor] provides significant functionality
// out of the box, and enforces that the tool conforms to the MCP spec:
// - The In type provides a default input schema for the tool, though it may
// be overridden in [AddTool].
// - The input value is automatically unmarshaled from req.Params.Arguments.
// - The input value is automatically validated against its input schema.
// Invalid input is rejected before getting to the handler.
// - 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.
// - 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
// error, and is therefore packed into CallToolResult.Content, with
// [IsError] set.
//
// For these reasons, most users can ignore the [CallToolRequest] argument and
// [CallToolResult] return values entirely. In fact, it is permissible to
// return a nil CallToolResult, if you only care about returning a output value
// or error. The effective result will be populated as described above.
type ToolHandlerFor[In, Out any] func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error)
// A serverTool is a tool definition that is bound to a tool handler.
type serverTool struct {
tool *Tool
handler ToolHandler
}
// applySchema validates whether data is valid JSON according to the provided
// schema, after applying schema defaults.
//
// Returns the JSON value augmented with defaults.
func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawMessage, error) {
// TODO: use reflection to create the struct type to unmarshal into.
// Separate validation from assignment.
// Use default JSON marshalling for validation.
//
// This avoids inconsistent representation due to custom marshallers, such as
// time.Time (issue #449).
//
// Additionally, unmarshalling into a map ensures that the resulting JSON is
// at least {}, even if data is empty. For example, arguments is technically
// an optional property of callToolParams, and we still want to apply the
// defaults in this case.
//
// TODO(rfindley): in which cases can resolved be nil?
if resolved != nil {
v := make(map[string]any)
if len(data) > 0 {
if err := internaljson.Unmarshal(data, &v); err != nil {
return nil, fmt.Errorf("unmarshaling arguments: %w", err)
}
}
if err := resolved.ApplyDefaults(&v); err != nil {
return nil, fmt.Errorf("applying schema defaults:\n%w", err)
}
if err := resolved.Validate(&v); err != nil {
return nil, err
}
// We must re-marshal with the default values applied.
var err error
data, err = json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("marshalling with defaults: %v", err)
}
}
return data, nil
}
// validateToolName checks whether name is a valid tool name, reporting a
// non-nil error if not.
func validateToolName(name string) error {
if name == "" {
return fmt.Errorf("tool name cannot be empty")
}
if len(name) > 128 {
return fmt.Errorf("tool name exceeds maximum length of 128 characters (current: %d)", len(name))
}
// For consistency with other SDKs, report characters in the order the appear
// in the name.
var invalidChars []string
seen := make(map[rune]bool)
for _, r := range name {
if !validToolNameRune(r) {
if !seen[r] {
invalidChars = append(invalidChars, fmt.Sprintf("%q", string(r)))
seen[r] = true
}
}
}
if len(invalidChars) > 0 {
return fmt.Errorf("tool name contains invalid characters: %s", strings.Join(invalidChars, ", "))
}
return nil
}
// validToolNameRune reports whether r is valid within tool names.
func validToolNameRune(r rune) bool {
return (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
r == '_' || r == '-' || r == '.'
}
+660
View File
@@ -0,0 +1,660 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"sync"
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
"github.com/modelcontextprotocol/go-sdk/internal/xcontext"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
)
// ErrConnectionClosed is returned when sending a message to a connection that
// is closed or in the process of closing.
var ErrConnectionClosed = errors.New("connection closed")
// ErrSessionMissing is returned when the session is known to not be present on
// the server.
var ErrSessionMissing = errors.New("session not found")
// A Transport is used to create a bidirectional connection between MCP client
// and server.
//
// Transports should be used for at most one call to [Server.Connect] or
// [Client.Connect].
type Transport interface {
// Connect returns the logical JSON-RPC connection..
//
// It is called exactly once by [Server.Connect] or [Client.Connect].
Connect(ctx context.Context) (Connection, error)
}
// A Connection is a logical bidirectional JSON-RPC connection.
type Connection interface {
// Read reads the next message to process off the connection.
//
// Connections must allow Read to be called concurrently with Close. In
// particular, calling Close should unblock a Read waiting for input.
Read(context.Context) (jsonrpc.Message, error)
// Write writes a new message to the connection.
//
// Write may be called concurrently, as calls or responses may occur
// concurrently in user code.
Write(context.Context, jsonrpc.Message) error
// Close closes the connection. It is implicitly called whenever a Read or
// Write fails.
//
// Close may be called multiple times, potentially concurrently.
Close() error
// TODO(#148): remove SessionID from this interface.
SessionID() string
}
// A ClientConnection is a [Connection] that is specific to the MCP client.
//
// If client connections implement this interface, they may receive information
// about changes to the client session.
//
// TODO: should this interface be exported?
type clientConnection interface {
Connection
// sessionUpdated is called whenever the client session state changes.
sessionUpdated(clientSessionState)
}
// A serverConnection is a Connection that is specific to the MCP server.
//
// If server connections implement this interface, they receive information
// about changes to the server session.
//
// TODO: should this interface be exported?
type serverConnection interface {
Connection
sessionUpdated(ServerSessionState)
}
// A StdioTransport is a [Transport] that communicates over stdin/stdout using
// newline-delimited JSON.
type StdioTransport struct{}
// Connect implements the [Transport] interface.
func (*StdioTransport) Connect(context.Context) (Connection, error) {
return newIOConn(rwc{os.Stdin, nopCloserWriter{os.Stdout}}), nil
}
// nopCloserWriter is an io.WriteCloser with a trivial Close method.
type nopCloserWriter struct {
io.Writer
}
func (nopCloserWriter) Close() error { return nil }
// An IOTransport is a [Transport] that communicates over separate
// io.ReadCloser and io.WriteCloser using newline-delimited JSON.
type IOTransport struct {
Reader io.ReadCloser
Writer io.WriteCloser
}
// Connect implements the [Transport] interface.
func (t *IOTransport) Connect(context.Context) (Connection, error) {
return newIOConn(rwc{t.Reader, t.Writer}), nil
}
// An InMemoryTransport is a [Transport] that communicates over an in-memory
// network connection, using newline-delimited JSON.
//
// InMemoryTransports should be constructed using [NewInMemoryTransports],
// which returns two transports connected to each other.
type InMemoryTransport struct {
rwc io.ReadWriteCloser
}
// Connect implements the [Transport] interface.
func (t *InMemoryTransport) Connect(context.Context) (Connection, error) {
return newIOConn(t.rwc), nil
}
// NewInMemoryTransports returns two [InMemoryTransport] objects that connect
// to each other.
//
// The resulting transports are symmetrical: use either to connect to a server,
// and then the other to connect to a client. Servers must be connected before
// clients, as the client initializes the MCP session during connection.
func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport) {
c1, c2 := net.Pipe()
return &InMemoryTransport{c1}, &InMemoryTransport{c2}
}
type binder[T handler, State any] interface {
// TODO(rfindley): the bind API has gotten too complicated. Simplify.
bind(Connection, *jsonrpc2.Connection, State, func()) T
disconnect(T)
}
type handler interface {
handle(ctx context.Context, req *jsonrpc.Request) (any, error)
}
func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, State], s State, onClose func()) (H, error) {
var zero H
mcpConn, err := t.Connect(ctx)
if err != nil {
return zero, err
}
// If logging is configured, write message logs.
reader, writer := jsonrpc2.Reader(mcpConn), jsonrpc2.Writer(mcpConn)
var (
h H
preempter canceller
)
bind := func(conn *jsonrpc2.Connection) jsonrpc2.Handler {
h = b.bind(mcpConn, conn, s, onClose)
preempter.conn = conn
return jsonrpc2.HandlerFunc(h.handle)
}
_ = jsonrpc2.NewConnection(ctx, jsonrpc2.ConnectionConfig{
Reader: reader,
Writer: writer,
Closer: mcpConn,
Bind: bind,
Preempter: &preempter,
OnDone: func() {
b.disconnect(h)
},
OnInternalError: func(err error) { log.Printf("jsonrpc2 error: %v", err) },
})
assert(preempter.conn != nil, "unbound preempter")
return h, nil
}
// A canceller is a jsonrpc2.Preempter that cancels in-flight requests on MCP
// cancelled notifications.
type canceller struct {
conn *jsonrpc2.Connection
}
// Preempt implements [jsonrpc2.Preempter].
func (c *canceller) Preempt(ctx context.Context, req *jsonrpc.Request) (result any, err error) {
if req.Method == notificationCancelled {
var params CancelledParams
if err := internaljson.Unmarshal(req.Params, &params); err != nil {
return nil, err
}
id, err := jsonrpc2.MakeID(params.RequestID)
if err != nil {
return nil, err
}
go c.conn.Cancel(id)
}
return nil, jsonrpc2.ErrNotHandled
}
// call executes and awaits a jsonrpc2 call on the given connection,
// translating errors into the mcp domain.
func call(ctx context.Context, conn *jsonrpc2.Connection, method string, params Params, result Result) error {
// The "%w"s in this function expose jsonrpc.Error as part of the API.
call := conn.Call(ctx, method, params)
err := call.Await(ctx, result)
switch {
case errors.Is(err, jsonrpc2.ErrClientClosing), errors.Is(err, jsonrpc2.ErrServerClosing):
return fmt.Errorf("%w: calling %q: %v", ErrConnectionClosed, method, err)
case ctx.Err() != nil:
// Notify the peer of cancellation.
err := conn.Notify(xcontext.Detach(ctx), notificationCancelled, &CancelledParams{
Reason: ctx.Err().Error(),
RequestID: call.ID().Raw(),
})
// By default, the jsonrpc2 library waits for graceful shutdown when the
// connection is closed, meaning it expects all outgoing and incoming
// requests to complete. However, for MCP this expectation is unrealistic,
// and can lead to hanging shutdown. For example, if a streamable client is
// killed, the server will not be able to detect this event, except via
// keepalive pings (if they are configured), and so outgoing calls may hang
// indefinitely.
//
// Therefore, we choose to eagerly retire calls, removing them from the
// outgoingCalls map, when the caller context is cancelled: if the caller
// will never receive the response, there's no need to track it.
conn.Retire(call, ctx.Err())
return errors.Join(ctx.Err(), err)
case err != nil:
return fmt.Errorf("calling %q: %w", method, err)
}
return nil
}
// A LoggingTransport is a [Transport] that delegates to another transport,
// writing RPC logs to an io.Writer.
type LoggingTransport struct {
Transport Transport
Writer io.Writer
}
// Connect connects the underlying transport, returning a [Connection] that writes
// logs to the configured destination.
func (t *LoggingTransport) Connect(ctx context.Context) (Connection, error) {
delegate, err := t.Transport.Connect(ctx)
if err != nil {
return nil, err
}
return &loggingConn{delegate: delegate, w: t.Writer}, nil
}
type loggingConn struct {
delegate Connection
mu sync.Mutex
w io.Writer
}
func (c *loggingConn) SessionID() string { return c.delegate.SessionID() }
// Read is a stream middleware that logs incoming messages.
func (s *loggingConn) Read(ctx context.Context) (jsonrpc.Message, error) {
msg, err := s.delegate.Read(ctx)
if err != nil {
s.mu.Lock()
fmt.Fprintf(s.w, "read error: %v\n", err)
s.mu.Unlock()
} else {
data, err := jsonrpc2.EncodeMessage(msg)
s.mu.Lock()
if err != nil {
fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err)
}
fmt.Fprintf(s.w, "read: %s\n", string(data))
s.mu.Unlock()
}
return msg, err
}
// Write is a stream middleware that logs outgoing messages.
func (s *loggingConn) Write(ctx context.Context, msg jsonrpc.Message) error {
err := s.delegate.Write(ctx, msg)
if err != nil {
s.mu.Lock()
fmt.Fprintf(s.w, "write error: %v\n", err)
s.mu.Unlock()
} else {
data, err := jsonrpc2.EncodeMessage(msg)
s.mu.Lock()
if err != nil {
fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err)
}
fmt.Fprintf(s.w, "write: %s\n", string(data))
s.mu.Unlock()
}
return err
}
func (s *loggingConn) Close() error {
return s.delegate.Close()
}
// A rwc binds an io.ReadCloser and io.WriteCloser together to create an
// io.ReadWriteCloser.
type rwc struct {
rc io.ReadCloser
wc io.WriteCloser
}
func (r rwc) Read(p []byte) (n int, err error) {
return r.rc.Read(p)
}
func (r rwc) Write(p []byte) (n int, err error) {
return r.wc.Write(p)
}
func (r rwc) Close() error {
rcErr := r.rc.Close()
var wcErr error
if r.wc != nil { // we only allow a nil writer in unit tests
wcErr = r.wc.Close()
}
return errors.Join(rcErr, wcErr)
}
// An ioConn is a transport that delimits messages with newlines across
// a bidirectional stream, and supports jsonrpc.2 message batching.
//
// See https://github.com/ndjson/ndjson-spec for discussion of newline
// delimited JSON.
//
// See [msgBatch] for more discussion of message batching.
type ioConn struct {
protocolVersion string // negotiated version, set during session initialization.
writeMu sync.Mutex // guards Write, which must be concurrency safe.
rwc io.ReadWriteCloser // the underlying stream
// incoming receives messages from the read loop started in [newIOConn].
incoming <-chan msgOrErr
// If outgoiBatch has a positive capacity, it will be used to batch requests
// and notifications before sending.
outgoingBatch []jsonrpc.Message
// Unread messages in the last batch. Since reads are serialized, there is no
// need to guard here.
queue []jsonrpc.Message
// batches correlate incoming requests to the batch in which they arrived.
// Since writes may be concurrent to reads, we need to guard this with a mutex.
batchMu sync.Mutex
batches map[jsonrpc2.ID]*msgBatch // lazily allocated
closeOnce sync.Once
closed chan struct{}
closeErr error
}
type msgOrErr struct {
msg json.RawMessage
err error
}
func newIOConn(rwc io.ReadWriteCloser) *ioConn {
var (
incoming = make(chan msgOrErr)
closed = make(chan struct{})
)
// Start a goroutine for reads, so that we can select on the incoming channel
// in [ioConn.Read] and unblock the read as soon as Close is called (see #224).
//
// This leaks a goroutine if rwc.Read does not unblock after it is closed,
// but that is unavoidable since AFAIK there is no (easy and portable) way to
// guarantee that reads of stdin are unblocked when closed.
go func() {
dec := json.NewDecoder(rwc)
for {
var raw json.RawMessage
err := dec.Decode(&raw)
// If decoding was successful, check for trailing data at the end of the stream.
if err == nil {
// Read the next byte to check if there is trailing data.
var tr [1]byte
if n, readErr := dec.Buffered().Read(tr[:]); n > 0 {
// If read byte is not a newline, it is an error.
// Support both Unix (\n) and Windows (\r\n) line endings.
if tr[0] != '\n' && tr[0] != '\r' {
err = fmt.Errorf("invalid trailing data at the end of stream")
}
} else if readErr != nil && readErr != io.EOF {
err = readErr
}
}
select {
case incoming <- msgOrErr{msg: raw, err: err}:
case <-closed:
return
}
if err != nil {
return
}
}
}()
return &ioConn{
rwc: rwc,
incoming: incoming,
closed: closed,
}
}
func (c *ioConn) SessionID() string { return "" }
func (c *ioConn) sessionUpdated(state ServerSessionState) {
protocolVersion := ""
if state.InitializeParams != nil {
protocolVersion = state.InitializeParams.ProtocolVersion
}
if protocolVersion == "" {
protocolVersion = protocolVersion20250326
}
c.protocolVersion = negotiatedVersion(protocolVersion)
}
// addBatch records a msgBatch for an incoming batch payload.
// It returns an error if batch is malformed, containing previously seen IDs.
//
// See [msgBatch] for more.
func (t *ioConn) addBatch(batch *msgBatch) error {
t.batchMu.Lock()
defer t.batchMu.Unlock()
for id := range batch.unresolved {
if _, ok := t.batches[id]; ok {
return fmt.Errorf("%w: batch contains previously seen request %v", jsonrpc2.ErrInvalidRequest, id.Raw())
}
}
for id := range batch.unresolved {
if t.batches == nil {
t.batches = make(map[jsonrpc2.ID]*msgBatch)
}
t.batches[id] = batch
}
return nil
}
// updateBatch records a response in the message batch tracking the
// corresponding incoming call, if any.
//
// The second result reports whether resp was part of a batch. If this is true,
// the first result is nil if the batch is still incomplete, or the full set of
// batch responses if resp completed the batch.
func (t *ioConn) updateBatch(resp *jsonrpc.Response) ([]*jsonrpc.Response, bool) {
t.batchMu.Lock()
defer t.batchMu.Unlock()
if batch, ok := t.batches[resp.ID]; ok {
idx, ok := batch.unresolved[resp.ID]
if !ok {
panic("internal error: inconsistent batches")
}
batch.responses[idx] = resp
delete(batch.unresolved, resp.ID)
delete(t.batches, resp.ID)
if len(batch.unresolved) == 0 {
return batch.responses, true
}
return nil, true
}
return nil, false
}
// A msgBatch records information about an incoming batch of jsonrpc.2 calls.
//
// The jsonrpc.2 spec (https://www.jsonrpc.org/specification#batch) says:
//
// "The Server should respond with an Array containing the corresponding
// Response objects, after all of the batch Request objects have been
// processed. A Response object SHOULD exist for each Request object, except
// that there SHOULD NOT be any Response objects for notifications. The Server
// MAY process a batch rpc call as a set of concurrent tasks, processing them
// in any order and with any width of parallelism."
//
// Therefore, a msgBatch keeps track of outstanding calls and their responses.
// When there are no unresolved calls, the response payload is sent.
type msgBatch struct {
unresolved map[jsonrpc2.ID]int
responses []*jsonrpc.Response
}
func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) {
// As a matter of principle, enforce that reads on a closed context return an
// error.
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
if len(t.queue) > 0 {
next := t.queue[0]
t.queue = t.queue[1:]
return next, nil
}
var raw json.RawMessage
select {
case <-ctx.Done():
return nil, ctx.Err()
case v := <-t.incoming:
if v.err != nil {
return nil, v.err
}
raw = v.msg
case <-t.closed:
return nil, io.EOF
}
msgs, batch, err := readBatch(raw)
if err != nil {
return nil, err
}
if batch && t.protocolVersion >= protocolVersion20250618 {
return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, t.protocolVersion)
}
t.queue = msgs[1:]
if batch {
var respBatch *msgBatch // track incoming requests in the batch
for _, msg := range msgs {
if req, ok := msg.(*jsonrpc.Request); ok {
if respBatch == nil {
respBatch = &msgBatch{
unresolved: make(map[jsonrpc2.ID]int),
}
}
if _, ok := respBatch.unresolved[req.ID]; ok {
return nil, fmt.Errorf("duplicate message ID %q", req.ID)
}
respBatch.unresolved[req.ID] = len(respBatch.responses)
respBatch.responses = append(respBatch.responses, nil)
}
}
if respBatch != nil {
// The batch contains one or more incoming requests to track.
if err := t.addBatch(respBatch); err != nil {
return nil, err
}
}
}
return msgs[0], err
}
// readBatch reads batch data, which may be either a single JSON-RPC message,
// or an array of JSON-RPC messages.
func readBatch(data []byte) (msgs []jsonrpc.Message, isBatch bool, _ error) {
// Try to read an array of messages first.
var rawBatch []json.RawMessage
if err := internaljson.Unmarshal(data, &rawBatch); err == nil {
if len(rawBatch) == 0 {
return nil, true, fmt.Errorf("empty batch")
}
for _, raw := range rawBatch {
msg, err := jsonrpc2.DecodeMessage(raw)
if err != nil {
return nil, true, err
}
msgs = append(msgs, msg)
}
return msgs, true, nil
}
// Try again with a single message.
msg, err := jsonrpc2.DecodeMessage(data)
return []jsonrpc.Message{msg}, false, err
}
func (t *ioConn) Write(ctx context.Context, msg jsonrpc.Message) error {
// As in [ioConn.Read], enforce that Writes on a closed context are an error.
select {
case <-ctx.Done():
return ctx.Err()
default:
}
t.writeMu.Lock()
defer t.writeMu.Unlock()
// Batching support: if msg is a Response, it may have completed a batch, so
// check that first. Otherwise, it is a request or notification, and we may
// want to collect it into a batch before sending, if we're configured to use
// outgoing batches.
if resp, ok := msg.(*jsonrpc.Response); ok {
if batch, ok := t.updateBatch(resp); ok {
if len(batch) > 0 {
data, err := marshalMessages(batch)
if err != nil {
return err
}
data = append(data, '\n')
_, err = t.rwc.Write(data)
return err
}
return nil
}
} else if len(t.outgoingBatch) < cap(t.outgoingBatch) {
t.outgoingBatch = append(t.outgoingBatch, msg)
if len(t.outgoingBatch) == cap(t.outgoingBatch) {
data, err := marshalMessages(t.outgoingBatch)
t.outgoingBatch = t.outgoingBatch[:0]
if err != nil {
return err
}
data = append(data, '\n')
_, err = t.rwc.Write(data)
return err
}
return nil
}
data, err := jsonrpc2.EncodeMessage(msg)
if err != nil {
return fmt.Errorf("marshaling message: %v", err)
}
data = append(data, '\n') // newline delimited
_, err = t.rwc.Write(data)
return err
}
func (t *ioConn) Close() error {
t.closeOnce.Do(func() {
t.closeErr = t.rwc.Close()
close(t.closed)
})
return t.closeErr
}
func marshalMessages[T jsonrpc.Message](msgs []T) ([]byte, error) {
var rawMsgs []json.RawMessage
for _, msg := range msgs {
raw, err := jsonrpc2.EncodeMessage(msg)
if err != nil {
return nil, fmt.Errorf("encoding batch message: %w", err)
}
rawMsgs = append(rawMsgs, raw)
}
return json.Marshal(rawMsgs)
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"encoding/json"
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
)
func assert(cond bool, msg string) {
if !cond {
panic(msg)
}
}
// remarshal marshals from to JSON, and then unmarshals into to, which must be
// a pointer type.
func remarshal(from, to any) error {
data, err := json.Marshal(from)
if err != nil {
return err
}
if err := internaljson.Unmarshal(data, to); err != nil {
return err
}
return nil
}