Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
90a0b1f
mcp: fix comments (#241)
cryo-zd Aug 5, 2025
31c18fb
docs: fix middleware example function signature (#236)
winterfx Aug 5, 2025
272a5ac
.github: add race and 1.25 tests
findleyr Aug 5, 2025
f4a9396
mcp: fix a bug causing premature termination of streams
findleyr Aug 5, 2025
be0bd77
mcp: test nil params crash vulnerability
davidleitw Aug 5, 2025
032f03b
jsonschema: add ForType function (#242)
jba Aug 5, 2025
a02c2ff
mcp: unblock reads of ioConn as soon as Close is called
findleyr Aug 4, 2025
bb9087d
mcp: handle bad trailing stdio input graciously (#179) (#192)
aryannr97 Aug 6, 2025
c132621
jsonschema: marshal to bools (#247)
jba Aug 6, 2025
e00c485
examples: add a 'listeverything' client example, and reorganize
findleyr Jul 30, 2025
b392875
internal/util: move FieldJSONInfo into jsonschema/util (#251)
samthanawalla Aug 6, 2025
5bd02a3
jsonschema: copy wrapf into jsonschema/util.go (#254)
samthanawalla Aug 6, 2025
4608401
mcp: simplify ServerSession.initialize
findleyr Aug 7, 2025
5ab63fe
mcp: don't mark the server sesion as initialized prematurely
findleyr Aug 7, 2025
e9e0da8
jsonschema: remove jsonschema code and depend on google/jsonschema-go…
samthanawalla Aug 7, 2025
8186bf3
mcp: lock down Params and Result
findleyr Aug 7, 2025
e2cf95c
auth: add OAuth authenticating middleware for server (#261)
jba Aug 8, 2025
c03cd68
internal/jsonrpc2: allow asynchronous writes
findleyr Aug 8, 2025
6ea7a6c
mcp: simplify and fix the streamable client transport
findleyr Aug 8, 2025
0375535
mcp: implement support for JSON responses in the MCP streamable server
findleyr Aug 8, 2025
388e000
.github: update go 1.25 builds to rc3 (#271)
findleyr Aug 9, 2025
4e413da
mcp: rename mcp{Params,Result} to is{Params,Result} (#270)
findleyr Aug 9, 2025
cccc086
mcp: clarify Server.Run use
jba Aug 9, 2025
be1ddf5
jsonrpc: paper over race in jsonrpc2 test
jba Aug 9, 2025
cb27392
.gitignore: add
krtkvrm Aug 11, 2025
119a583
mcp: address some comments from #234 (#248)
findleyr Aug 11, 2025
dae8853
mcp: support stateless streamable sessions (#277)
findleyr Aug 11, 2025
679f777
Update pull_request_template.md (#287)
samthanawalla Aug 12, 2025
e097918
mcp: make transports open structs
findleyr Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
### PR Tips
### PR Guideline

Typically, PRs should consist of a single commit, and so should generally follow
the [rules for Go commit messages](https://go.dev/wiki/CommitMessage), with the following
changes and additions:
the [rules for Go commit messages](https://go.dev/wiki/CommitMessage).

- Markdown is allowed.
You **must** follow the form:

- For a pervasive change, use "all" in the title instead of a package name.
```
net/http: handle foo when bar

[longer description here in the body]

Fixes #12345
```
Notably, for the subject (the first line of description):

- the name of the package affected by the change goes before the colon
- the part after the colon uses the verb tense + phrase that completes the blank in, “this change modifies this package to ___________”
- the verb after the colon is lowercase
- there is no trailing period
- it should be kept as short as possible

Additionally:

- Markdown is allowed.
- For a pervasive change, use "all" in the title instead of a package name.
- The PR description should provide context (why this change?) and describe the changes
at a high level. Changes that are obvious from the diffs don't need to be mentioned.
28 changes: 20 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
name: Test
on:
# Manual trigger
workflow_dispatch:
workflow_dispatch:
push:
branches: main
pull_request:

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v5
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
Expand All @@ -26,18 +26,30 @@ jobs:
exit 1
fi
echo "All Go files are properly formatted"

test:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.23', '1.24' ]
go: [ '1.23', '1.24', '1.25.0-rc.3' ]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Check out code
uses: actions/checkout@v4
- name: Test
run: go test -v ./...

race-test:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Test with -race
run: go test -v -race ./...
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Builds
*.exe
*.exe~
dist/
build/
bin/
*.tmp

# IDE files
.vscode/
*.code-workspace
.idea/
*~

# Go Specific
*.prof
*.pprof
*.out
*.coverage
coverage.txt
coverage.html

# OS generated files
# macOS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Windows
Desktop.ini
$RECYCLE.BIN/

# Linux
.nfs*
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ func main() {
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

// Connect to a server over stdin/stdout
transport := mcp.NewCommandTransport(exec.Command("myserver"))
session, err := client.Connect(ctx, transport)
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -127,7 +127,7 @@ func main() {

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.NewStdioTransport()); err != nil {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Expand Down
96 changes: 96 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 auth

import (
"context"
"errors"
"net/http"
"slices"
"strings"
"time"
)

type TokenInfo struct {
Scopes []string
Expiration time.Time
}

type TokenVerifier func(ctx context.Context, token string) (*TokenInfo, error)

type RequireBearerTokenOptions struct {
Scopes []string
ResourceMetadataURL string
}

var ErrInvalidToken = errors.New("invalid token")

type tokenInfoKey struct{}

// RequireBearerToken returns a piece of middleware that verifies a bearer token using the verifier.
// If verification succeeds, the [TokenInfo] is added to the request's context and the request proceeds.
// If verification fails, the request fails with a 401 Unauthenticated, and the WWW-Authenticate header
// is populated to enable [protected resource metadata].
//
// [protected resource metadata]: https://datatracker.ietf.org/doc/rfc9728
func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) func(http.Handler) http.Handler {
// Based on typescript-sdk/src/server/auth/middleware/bearerAuth.ts.

return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenInfo, errmsg, code := verify(r.Context(), verifier, opts, r.Header.Get("Authorization"))
if code != 0 {
if code == http.StatusUnauthorized || code == http.StatusForbidden {
if opts != nil && opts.ResourceMetadataURL != "" {
w.Header().Add("WWW-Authenticate", "Bearer resource_metadata="+opts.ResourceMetadataURL)
}
}
http.Error(w, errmsg, code)
return
}
r = r.WithContext(context.WithValue(r.Context(), tokenInfoKey{}, tokenInfo))
handler.ServeHTTP(w, r)
})
}
}

func verify(ctx context.Context, verifier TokenVerifier, opts *RequireBearerTokenOptions, authHeader string) (_ *TokenInfo, errmsg string, code int) {
// Extract bearer token.
fields := strings.Fields(authHeader)
if len(fields) != 2 || strings.ToLower(fields[0]) != "bearer" {
return nil, "no bearer token", http.StatusUnauthorized
}

// Verify the token and get information from it.
tokenInfo, err := verifier(ctx, fields[1])
if err != nil {
if errors.Is(err, ErrInvalidToken) {
return nil, err.Error(), http.StatusUnauthorized
}
// TODO: the TS SDK distinguishes another error, OAuthError, and returns a 400.
// Investigate how that works.
// See typescript-sdk/src/server/auth/middleware/bearerAuth.ts.
return nil, err.Error(), http.StatusInternalServerError
}

// Check scopes.
if opts != nil {
// Note: quadratic, but N is small.
for _, s := range opts.Scopes {
if !slices.Contains(tokenInfo.Scopes, s) {
return nil, "insufficient scope", http.StatusForbidden
}
}
}

// Check expiration.
if tokenInfo.Expiration.IsZero() {
return nil, "token missing expiration", http.StatusUnauthorized
}
if tokenInfo.Expiration.Before(time.Now()) {
return nil, "token expired", http.StatusUnauthorized
}
return tokenInfo, "", 0
}
70 changes: 70 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 auth

import (
"context"
"errors"
"testing"
"time"
)

func TestVerify(t *testing.T) {
ctx := context.Background()
verifier := func(_ context.Context, token string) (*TokenInfo, error) {
switch token {
case "valid":
return &TokenInfo{Expiration: time.Now().Add(time.Hour)}, nil
case "invalid":
return nil, ErrInvalidToken
case "noexp":
return &TokenInfo{}, nil
case "expired":
return &TokenInfo{Expiration: time.Now().Add(-time.Hour)}, nil
default:
return nil, errors.New("unknown")
}
}

for _, tt := range []struct {
name string
opts *RequireBearerTokenOptions
header string
wantMsg string
wantCode int
}{
{
"valid", nil, "Bearer valid",
"", 0,
},
{
"bad header", nil, "Barer valid",
"no bearer token", 401,
},
{
"invalid", nil, "bearer invalid",
"invalid token", 401,
},
{
"no expiration", nil, "Bearer noexp",
"token missing expiration", 401,
},
{
"expired", nil, "Bearer expired",
"token expired", 401,
},
{
"missing scope", &RequireBearerTokenOptions{Scopes: []string{"s1"}}, "Bearer valid",
"insufficient scope", 403,
},
} {
t.Run(tt.name, func(t *testing.T) {
_, gotMsg, gotCode := verify(ctx, verifier, tt.opts, tt.header)
if gotMsg != tt.wantMsg || gotCode != tt.wantCode {
t.Errorf("got (%q, %d), want (%q, %d)", gotMsg, gotCode, tt.wantMsg, tt.wantCode)
}
})
}
}
Loading
Loading