diff --git a/README.md b/README.md index 55cdfc5d..81af0b09 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,13 @@ -# MCP Go SDK v0.8.0 +# MCP Go SDK [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/modelcontextprotocol/go-sdk) -***BREAKING CHANGES*** - -This version contains minor breaking changes. -See the [release notes]( -https://github.com/modelcontextprotocol/go-sdk/releases/tag/v0.8.0) for details. - [![PkgGoDev](https://pkg.go.dev/badge/github.com/modelcontextprotocol/go-sdk)](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) This repository contains an implementation of the official Go software development kit (SDK) for the Model Context Protocol (MCP). -> [!IMPORTANT] -> The SDK is in release-candidate state, and is going to be tagged v1.0.0 -> soon (see https://github.com/modelcontextprotocol/go-sdk/issues/328). -> We do not anticipate significant API changes or instability. Please use it -> and [file issues](https://github.com/modelcontextprotocol/go-sdk/issues/new/choose). - ## Package / Feature documentation The SDK consists of several importable packages: diff --git a/docs/README.md b/docs/README.md index b4268c85..9f325075 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,21 +15,21 @@ protocol. 1. [Authorization](protocol.md#authorization) 1. [Security](protocol.md#security) 1. [Utilities](protocol.md#utilities) - 1. [Cancellation](utilities.md#cancellation) - 1. [Ping](utilities.md#ping) - 1. [Progress](utilities.md#progress) + 1. [Cancellation](protocol.md#cancellation) + 1. [Ping](protocol.md#ping) + 1. [Progress](protocol.md#progress) ## Client Features 1. [Roots](client.md#roots) 1. [Sampling](client.md#sampling) -1. [Elicitation](clients.md#elicitation) +1. [Elicitation](client.md#elicitation) ## Server Features 1. [Prompts](server.md#prompts) 1. [Resources](server.md#resources) -1. [Tools](tools.md) +1. [Tools](server.md#tools) 1. [Utilities](server.md#utilities) 1. [Completion](server.md#completion) 1. [Logging](server.md#logging) diff --git a/internal/docs/README.src.md b/internal/docs/README.src.md index b252f943..7f7b2247 100644 --- a/internal/docs/README.src.md +++ b/internal/docs/README.src.md @@ -14,21 +14,21 @@ protocol. 1. [Authorization](protocol.md#authorization) 1. [Security](protocol.md#security) 1. [Utilities](protocol.md#utilities) - 1. [Cancellation](utilities.md#cancellation) - 1. [Ping](utilities.md#ping) - 1. [Progress](utilities.md#progress) + 1. [Cancellation](protocol.md#cancellation) + 1. [Ping](protocol.md#ping) + 1. [Progress](protocol.md#progress) ## Client Features 1. [Roots](client.md#roots) 1. [Sampling](client.md#sampling) -1. [Elicitation](clients.md#elicitation) +1. [Elicitation](client.md#elicitation) ## Server Features 1. [Prompts](server.md#prompts) 1. [Resources](server.md#resources) -1. [Tools](tools.md) +1. [Tools](server.md#tools) 1. [Utilities](server.md#utilities) 1. [Completion](server.md#completion) 1. [Logging](server.md#logging) diff --git a/internal/oauthex/oauth2_test.go b/internal/oauthex/oauth2_test.go index 9c3da156..587b676d 100644 --- a/internal/oauthex/oauth2_test.go +++ b/internal/oauthex/oauth2_test.go @@ -268,3 +268,76 @@ func (h *fakeResourceHandler) installHandlers(serverURL string) { } })) } + +func Test_checkURLScheme(t *testing.T) { + tests := []struct { + name string + u string + wantErr bool + }{ + { + name: "empty url", + u: "", + wantErr: false, + }, + { + name: "valid http", + u: "http://example.com", + wantErr: false, + }, + { + name: "valid https", + u: "https://example.com", + wantErr: false, + }, + { + name: "valid https with uppercase scheme", + u: "HTTPS://example.com", + wantErr: false, + }, + { + name: "url with no scheme", + u: "example.com", + wantErr: false, + }, + { + name: "disallowed javascript scheme", + u: "javascript:alert('XSS')", + wantErr: true, + }, + { + name: "disallowed javascript scheme with uppercase", + u: "Javascript:alert('XSS')", + wantErr: true, + }, + { + name: "disallowed data scheme", + u: "data:text/html,", + wantErr: true, + }, + { + name: "disallowed vbscript scheme", + u: "vbscript:msgbox(\"XSS\")", + wantErr: true, + }, + { + name: "invalid url format", + u: "://invalid-url", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotErr := checkURLScheme(tt.u) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("checkURLScheme() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("checkURLScheme() succeeded unexpectedly") + } + }) + } +} diff --git a/internal/oauthex/resource_meta_test.go b/internal/oauthex/resource_meta_test.go new file mode 100644 index 00000000..7c05abe9 --- /dev/null +++ b/internal/oauthex/resource_meta_test.go @@ -0,0 +1,252 @@ +// 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 oauthex + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" +) + +func Test_getPRM(t *testing.T) { + ctx := context.Background() + const wantResource = "https://resource.example.com" + + prmOK := &ProtectedResourceMetadata{ + Resource: wantResource, + AuthorizationServers: []string{"https://as.example.com"}, + } + prmOKJSON, err := json.Marshal(prmOK) + if err != nil { + t.Fatal(err) + } + + prmMismatchedResource := &ProtectedResourceMetadata{ + Resource: "https://wrong.example.com", + } + prmMismatchedResourceJSON, err := json.Marshal(prmMismatchedResource) + if err != nil { + t.Fatal(err) + } + + prmBadAuthServer := &ProtectedResourceMetadata{ + Resource: wantResource, + AuthorizationServers: []string{"javascript:alert(1)"}, + } + prmBadAuthServerJSON, err := json.Marshal(prmBadAuthServer) + if err != nil { + t.Fatal(err) + } + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ok": + w.Header().Set("Content-Type", "application/json") + w.Write(prmOKJSON) + case "/mismatched-resource": + w.Header().Set("Content-Type", "application/json") + w.Write(prmMismatchedResourceJSON) + case "/bad-auth-server": + w.Header().Set("Content-Type", "application/json") + w.Write(prmBadAuthServerJSON) + case "/bad-status": + http.Error(w, "not found", http.StatusNotFound) + case "/bad-content-type": + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("not json")) + case "/bad-json": + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("not-json")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + tests := []struct { + name string + purl string + wantResource string + client *http.Client + want *ProtectedResourceMetadata + wantErr string + }{ + { + name: "non-https url", + purl: "http://example.com", + wantResource: wantResource, + wantErr: `resource URL "http://example.com" does not use HTTPS`, + }, + { + name: "http get error", + purl: "https://localhost:0", // Invalid port to cause connection error + wantResource: wantResource, + wantErr: `Get "https://localhost:0": dial tcp`, + }, + { + name: "bad status code", + purl: server.URL + "/bad-status", + wantResource: wantResource, + client: server.Client(), + wantErr: "bad status 404 Not Found", + }, + { + name: "bad content type", + purl: server.URL + "/bad-content-type", + wantResource: wantResource, + client: server.Client(), + wantErr: `bad content type "text/plain"`, + }, + { + name: "bad json", + purl: server.URL + "/bad-json", + wantResource: wantResource, + client: server.Client(), + wantErr: "invalid character 'o' in literal null (expecting 'u')", + }, + { + name: "mismatched resource", + purl: server.URL + "/mismatched-resource", + wantResource: wantResource, + client: server.Client(), + wantErr: `got metadata resource "https://wrong.example.com", want "https://resource.example.com"`, + }, + { + name: "bad auth server url scheme", + purl: server.URL + "/bad-auth-server", + wantResource: wantResource, + client: server.Client(), + wantErr: `URL has disallowed scheme "javascript"`, + }, + { + name: "success", + purl: server.URL + "/ok", + wantResource: wantResource, + client: server.Client(), + want: prmOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getPRM(ctx, tt.purl, tt.client, tt.wantResource) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("getPRM() error = nil, wantErr %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("getPRM() error = %q, want to contain %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("getPRM() unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPRM() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetProtectedResourceMetadataFromHeader(t *testing.T) { + ctx := context.Background() + + prmOK := &ProtectedResourceMetadata{ + Resource: "https://resource.example.com/prm", + } + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // The resource URL in prmOK doesn't match the server URL, so we need to adjust it. + // prmOK.Resource = server.URL + "/prm" + prmOKJSON, _ := json.Marshal(prmOK) + + if r.URL.Path == "/prm" { + w.Header().Set("Content-Type", "application/json") + w.Write(prmOKJSON) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + httpsURL := server.URL + "/prm" + prmOK.Resource = server.URL + "/prm" + + // The expected resource must match the dynamic URL of the test server. + wantPRM := &ProtectedResourceMetadata{ + Resource: httpsURL, + } + + tests := []struct { + name string + header http.Header + client *http.Client + want *ProtectedResourceMetadata + wantErr string + }{ + { + name: "no www-authenticate header", + header: http.Header{}, + want: nil, + }, + { + name: "empty www-authenticate header", + header: http.Header{ + "Www-Authenticate": []string{}, + }, + want: nil, + }, + { + name: "no resource_metadata parameter", + header: http.Header{ + "Www-Authenticate": []string{`Bearer realm="example.com"`}, + }, + want: nil, + }, + { + name: "success", + header: http.Header{ + "Www-Authenticate": []string{fmt.Sprintf(`Bearer resource_metadata="%s"`, httpsURL)}, + }, + client: server.Client(), + want: wantPRM, + }, + { + name: "getPRM fails with non-https url", + header: http.Header{ + "Www-Authenticate": []string{`Bearer resource_metadata="http://insecure.com"`}, + }, + client: server.Client(), + wantErr: `resource URL "http://insecure.com" does not use HTTPS`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetProtectedResourceMetadataFromHeader(ctx, tt.header, tt.client) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("GetProtectedResourceMetadataFromHeader() error = nil, wantErr %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("GetProtectedResourceMetadataFromHeader() error = %q, want to contain %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("GetProtectedResourceMetadataFromHeader() unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetProtectedResourceMetadataFromHeader() got = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/internal/readme/README.src.md b/internal/readme/README.src.md index 3fd305a4..e83c5fc4 100644 --- a/internal/readme/README.src.md +++ b/internal/readme/README.src.md @@ -1,24 +1,12 @@ -# MCP Go SDK v0.8.0 +# MCP Go SDK [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/modelcontextprotocol/go-sdk) -***BREAKING CHANGES*** - -This version contains minor breaking changes. -See the [release notes]( -https://github.com/modelcontextprotocol/go-sdk/releases/tag/v0.8.0) for details. - [![PkgGoDev](https://pkg.go.dev/badge/github.com/modelcontextprotocol/go-sdk)](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) This repository contains an implementation of the official Go software development kit (SDK) for the Model Context Protocol (MCP). -> [!IMPORTANT] -> The SDK is in release-candidate state, and is going to be tagged v1.0.0 -> soon (see https://github.com/modelcontextprotocol/go-sdk/issues/328). -> We do not anticipate significant API changes or instability. Please use it -> and [file issues](https://github.com/modelcontextprotocol/go-sdk/issues/new/choose). - ## Package / Feature documentation The SDK consists of several importable packages: diff --git a/mcp/transport.go b/mcp/transport.go index d2109e7d..a9cfa371 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -230,7 +230,7 @@ func (s *loggingConn) Read(ctx context.Context) (jsonrpc.Message, error) { if err != nil { s.mu.Lock() - fmt.Fprintf(s.w, "read error: %v", err) + fmt.Fprintf(s.w, "read error: %v\n", err) s.mu.Unlock() } else { data, err := jsonrpc2.EncodeMessage(msg) @@ -250,7 +250,7 @@ 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", err) + fmt.Fprintf(s.w, "write error: %v\n", err) s.mu.Unlock() } else { data, err := jsonrpc2.EncodeMessage(msg)