Mirror of https://github.com/modelcontextprotocol/go-sdk — The official Go SDK for Model Context Protocol servers and clients. Maintained in collaboration with Google.
  • Go 99.7%
  • Shell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Aircg 4608cda997
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
Conformance Tests / server-conformance (push) Has been cancelled
Conformance Tests / client-conformance (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Test / lint (push) Has been cancelled
Test / test (1.25) (push) Has been cancelled
Test / test (1.26) (push) Has been cancelled
Test / race-test (push) Has been cancelled
mcp: retire only the subscriptions a listen registered (#1275)
*Narrowed after review — see the discussion below. The earlier revision
changed the
registry keying; this one changes only the teardown.*

### The problem

`subscriptionsListen` registers a session according to what the listen
asked for:

```go
if allowed.ToolsListChanged {
    s.toolChangeSubscriptions[req.Session] = requestID
}
if allowed.PromptsListChanged {
    s.promptChangeSubscriptions[req.Session] = requestID
}
if allowed.ResourcesListChanged {
    s.resourceChangeSubscriptions[req.Session] = requestID
}
```

Its deferred cleanup does not:

```go
delete(s.toolChangeSubscriptions, req.Session)
delete(s.promptChangeSubscriptions, req.Session)
delete(s.resourceChangeSubscriptions, req.Session)
```

It runs whether or not this listen ever wrote those registries. A
session can
hold several listens at once, so the first one to unwind takes the
others'
registrations with it, and those streams stay open while receiving
nothing.

The scope is the persistent-session transports, stdio and in-memory.
Over
streamable HTTP the registries cannot collide: a stateful handler
refuses
SEP-2575 requests outright with `CodeUnsupportedProtocolVersion`, and a
stateless one builds a temporary session per POST and closes it when the
request ends.

Reachable through this SDK's own client, on stdio:

1. `Client.Connect` opens the auto-listen for list-changed
notifications.
2. `ClientSession.Subscribe(uri)` opens a second listen, carrying only a
   resource subscription.
3. `ClientSession.Unsubscribe(uri)` retires that second listen.
4. Its cleanup deletes `toolChangeSubscriptions[session]`, which belongs
to the
   auto-listen.
5. The auto-listen is still open and never receives `tools/list_changed`
again.

### How I pinned it down

A diagnostic run over the three states, reading the registries and
`ServerSession.listenIDs` directly:

```
after connect      toolSub=2       promptSub=2       resSub[r1]=false  listenIDs=[{2}]
after Subscribe    toolSub=2       promptSub=2       resSub[r1]=true   listenIDs=[{2} {3}]
after Unsubscribe  toolSub=<nil>   promptSub=<nil>   resSub[r1]=false  listenIDs=[{2}]
```

Line 2 rules out the second listen having overwritten the entry:
`toolSub` is
still 2, listen 3 never wrote that registry. Line 3 rules out the client
cancelling the wrong stream: `listenIDs` still holds listen 2, so the
auto-listen is alive on the server, parked on `<-ctx.Done()`. What
changed is
only that its registration is gone, deleted by listen 3's teardown.

### The change

Guard each delete with the condition that guarded the registration, so a
listen
retires what it registered and nothing else.

### Validation

Two tests, next to the existing `TestSubscriptionsListen_*` ones and
reusing
their fixtures. Against `826e653` without the change:

```
--- FAIL: TestSubscriptionsListen_TeardownKeepsOtherListens (5.01s)
    mcp_test.go:3442: timed out waiting for "tool"
--- PASS: TestSubscriptionsListen_TeardownRetiresOwnRegistration (0.01s)
```

The second passes before and after on purpose: it asserts that the only
listen
on a session does still get retired, so that the guards cannot be
tightened
into a registry leak. The eight pre-existing `TestSubscriptionsListen_*`
tests
pass in both runs, and all ten pass with the change.

Run on the same commit, before and after: `go test ./...` and
`go test -race ./...` ok with no races, `gofmt -l .`, `go vet ./...` and
`staticcheck v0.6.1` clean. Also `go test ./...` on Go 1.25.0 and 1.26.8
to
match the CI matrix, and `go generate ./internal/readme ./internal/docs`
produces no diff. Environment: Intel Core i5-1038NG7, macOS 14.

### Known limitations

Two cases are outside this change, both needing two listens on one
session that
overlap:

- Two listens opting in to the same notification type.
`allowed.ToolsListChanged`
is true for both, so the registration overwrites and this teardown still
  retires the survivor's entry.
- Two listens subscribed to the same URI.
`resourceSubscriptions[uri][session]`
  overwrites the same way, and the deferred `unsubscribe` deletes it.

Neither is reachable from this SDK's client — `Subscribe` dedups per URI
and
there is no exported way to open a second list-changed listen — so both
require
a non-Go client on stdio. I have tests for them and am happy to open a
separate
issue if you want them tracked.

---

AI assistance was used for reading through the code, the diagnostic and
regression tests, and running the verification above. The root cause
analysis
and the design decisions are mine, and I can explain every line without
it.

Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
2026-09-18 08:42:05 +00:00
.agents/skills/fix-bug chore: configure a simple AGENTS.md file and a skill for fixing GitHu… (#810) 2026-02-19 15:22:12 +01:00
.devcontainer feat(devcontainer): Adds Dev Container configuration (#154) 2025-07-30 12:54:41 -04:00
.github mcp: update conformance tests (#1231) 2026-09-03 11:27:42 +02:00
auth auth: strip a terminating slash from the issuer before building metadata URLs (#1245) 2026-09-07 09:31:45 +00:00
conformance mcp: update conformance tests (#1231) 2026-09-03 11:27:42 +02:00
design feat: multi-round-trip request implementation (SEP-2322) (#950) 2026-05-29 09:29:34 +02:00
docs mcp: preserve structured content number precision (#1239) 2026-09-16 09:22:16 +02:00
examples mcp: add HasParams so middleware can tell whether params are present (#1269) 2026-09-14 18:45:48 +02:00
internal mcp: preserve structured content number precision (#1239) 2026-09-16 09:22:16 +02:00
jsonrpc all: expose JSON-RPC error codes, and support URLElicitationRequired errors (#655) 2025-12-01 11:29:17 -05:00
mcp mcp: retire only the subscriptions a listen registered (#1275) 2026-09-18 08:42:05 +00:00
oauthex oauthex: build defaultDiscoveryTransport lazily instead of at import (#1278) 2026-09-18 09:41:05 +02:00
scripts ci: fix nightly runs (#1086) 2026-07-14 16:24:47 +02:00
.gitignore .gitignore: add 2025-08-11 08:33:50 -04:00
AGENTS.md chore: configure a simple AGENTS.md file and a skill for fixing GitHu… (#810) 2026-02-19 15:22:12 +01:00
CONTRIBUTING.md mcp: add docs for tier 1 assessment (#1195) 2026-08-24 12:00:59 +02:00
copyright_test.go mcp: update JSON marshaling to not HTML-escape messages (#769) 2026-01-30 17:13:30 +01:00
go.mod feat: multi-round-trip request implementation (SEP-2322) (#950) 2026-05-29 09:29:34 +02:00
go.sum feat: multi-round-trip request implementation (SEP-2322) (#950) 2026-05-29 09:29:34 +02:00
LICENSE chore: update licensing to Apache 2.0 for new contributions (#750) 2026-01-28 09:17:41 +01:00
mkdocs.yaml Add documentation backed by mkdocs (#883) 2026-04-09 14:25:43 +02:00
README.md mcp: add documentation for new protocol version (#1019) 2026-06-23 17:07:38 +02:00
ROADMAP.md chore: add ROADMAP.md (#788) 2026-02-13 10:39:53 +01:00
SECURITY.md chore: update SECURITY.md to use GitHub Security Advisories (#809) 2026-02-18 22:24:53 +01:00

MCP Go SDK

Open in GitHub Codespaces

PkgGoDev OpenSSF Scorecard

This repository contains an implementation of the official Go software development kit (SDK) for the Model Context Protocol (MCP).

Package / Feature documentation

The SDK consists of several importable packages:

The SDK endeavors to implement the full MCP spec. The docs/ directory contains feature documentation, mapping the MCP spec to the packages above.

Version Compatibility

The following table shows which versions of the Go SDK support which versions of the MCP specification:

SDK Version Latest MCP Spec All Supported MCP Specs
v1.7.0+ 2026-07-28 2026-07-28, 2025-11-25*, 2025-06-18, 2025-03-26, 2024-11-05
v1.4.0 - v1.6.1 2025-11-25* 2025-11-25*, 2025-06-18, 2025-03-26, 2024-11-05
v1.2.0 - v1.3.1 2025-11-25** 2025-11-25**, 2025-06-18, 2025-03-26, 2024-11-05
v1.0.0 - v1.1.0 2025-06-18 2025-06-18, 2025-03-26, 2024-11-05

* Client side OAuth has experimental support.

** Partial support for 2025-11-25 (client side OAuth and Sampling with tools not available).

The roots, sampling, and logging features are deprecated as of protocol version 2026-07-28 by SEP-2577. The SDK continues to support them for compatibility during the deprecation window (at least twelve months). See the individual feature documentation for migration guidance.

New releases of the SDK target only supported versions of Go. See https://go.dev/doc/devel/release#policy for more information.

Getting started

To get started creating an MCP server, create an mcp.Server instance, add features to it, and then run it over an mcp.Transport. For example, this server adds a single simple tool, and then connects it to clients over stdin/stdout:

package main

import (
	"context"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type Input struct {
	Name string `json:"name" jsonschema:"the name of the person to greet"`
}

type Output struct {
	Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}

func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
	*mcp.CallToolResult,
	Output,
	error,
) {
	return nil, Output{Greeting: "Hi " + input.Name}, nil
}

func main() {
	// Create a server with a single tool.
	server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
	// Run the server over stdin/stdout, until the client disconnects.
	if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
		log.Fatal(err)
	}
}

To communicate with that server, create an mcp.Client and connect it to the corresponding server, by running the server command and communicating over its stdin/stdout:

package main

import (
	"context"
	"log"
	"os/exec"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
	ctx := context.Background()

	// Create a new client, with no features.
	client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

	// Connect to a server over stdin/stdout.
	transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
	session, err := client.Connect(ctx, transport, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	// Call a tool on the server.
	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)
	}
	if res.IsError {
		log.Fatal("tool failed")
	}
	for _, c := range res.Content {
		log.Print(c.(*mcp.TextContent).Text)
	}
}

The examples/ directory contains more example clients and servers.

Contributing

We welcome contributions to the SDK! Please see CONTRIBUTING.md for details of how to contribute.

Acknowledgements / Alternatives

Several third party Go MCP SDKs inspired the development and design of this official SDK, and continue to be viable alternatives, notably mcp-go, originally authored by Ed Zynda. We are grateful to Ed as well as the other contributors to mcp-go, and to authors and contributors of other SDKs such as mcp-golang and go-mcp. Thanks to their work, there is a thriving ecosystem of Go MCP clients and servers.

License

This project is licensed under Apache 2.0 for new contributions, with existing code under MIT - see the LICENSE file for details.