Repository: modelcontextprotocol/go-sdk

Issue:
jsonschema: support additional keywords
The JSON Schema spec allows additional keywords, beyond those in the various specified vocabularies.

The `Schema` type should support that. The obvious way is to add a map[string]any field. We then have to ensure that the map keys are JSON-marshaled at top level, along with the other keywords. That is a bit tricky, but we have [un]marshalStructWithMap in mcp/util.go.

Hints from issue comments:
@albertsundjaja: hello! I'm interested in contributing to accelerate the release :)

I have taken a look at the source code and my plan for tackling this is to

1. add a `map[string]any` field in `Schema` type defined in `jsonschema/schema.go`
2. extend the `[UN]MarshalJSON` functions to use the `[un]marshalStructWithMap`
- the `Extra` fields will hold the extra fields that are not defined in `Schema` but during marshal, Extra will be flattened to top level key-value pairs
3. add / refactor necessary unit tests for the above (unknown fields are no longer dropped)

Let me know if I can pick this up!@jba: Thanks! I assigned you.
You'll have to copy the [un]marshalStructWithMap functions into jsonschema.@jba: @qiaodev if Schema had an Extra field, and you could do:
```
schema.Extra["propertyOrdering"] = []string{"a", "b", "c"}
```
would that work for genai?@qiaodev: > would that work for genai?

Thanks for the effort. It works for genai.



Solution patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..be13052
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+### PR Tips
+
+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:
+
+- 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.
diff --git a/design/design.md b/design/design.md
index 4ade34c..b52e9c1 100644
--- a/design/design.md
+++ b/design/design.md
@@ -470,6 +470,10 @@ server.AddReceivingMiddleware(withLogging)
 
 **Differences from mcp-go**: Version 0.26.0 of mcp-go defines 24 server hooks. Each hook consists of a field in the `Hooks` struct, a `Hooks.Add` method, and a type for the hook function. These are rarely used. The most common is `OnError`, which occurs fewer than ten times in open-source code.
 
+#### Rate Limiting
+
+Rate limiting can be configured using middleware. Please see [examples/rate-limiting](<https://github.com/modelcontextprotocol/go-sdk/tree/main/examples/rate-limiting>] for an example on how to implement this.
+
 ### Errors
 
 With the exception of tool handler errors, protocol errors are handled transparently as Go errors: errors in server-side feature handlers are propagated as errors from calls from the `ClientSession`, and vice-versa.
@@ -863,7 +867,7 @@ type ServerOptions struct {
 }
 ```
 
-#### Securty Considerations
+#### Security Considerations
 
 Implementors of the CompletionHandler MUST adhere to the following security guidelines:
 
@@ -952,7 +956,7 @@ In addition to the `List` methods, the SDK provides an iterator method for each
 
 # Governance and Community
 
-While the sections above propose an initial implementation of the Go SDK, MCP is evolving rapidly. SDKs need to keep pace, by implementing changes to the spec, fixing bugs, and accomodating new and emerging use-cases. This section proposes how the SDK project can be managed so that it can change safely and transparently.
+While the sections above propose an initial implementation of the Go SDK, MCP is evolving rapidly. SDKs need to keep pace, by implementing changes to the spec, fixing bugs, and accommodating new and emerging use-cases. This section proposes how the SDK project can be managed so that it can change safely and transparently.
 
 Initially, the Go SDK repository will be administered by the Go team and Anthropic, and they will be the Approvers (the set of people able to merge PRs to the SDK). The policies here are also intended to satisfy necessary constraints of the Go team's participation in the project.
 
@@ -980,7 +984,7 @@ A proposal is an issue that proposes a new API for the SDK, or a change to the s
 
 Proposals that are straightforward and uncontroversial may be approved based on GitHub discussion. However, proposals that are deemed to be sufficiently unclear or complicated will be deferred to a regular steering meeting (see below).
 
-This process is similar to the [Go proposal process](https://github.com/golang/proposal), but is necessarily lighter weight to accomodate the greater rate of change expected for the SDK.
+This process is similar to the [Go proposal process](https://github.com/golang/proposal), but is necessarily lighter weight to accommodate the greater rate of change expected for the SDK.
 
 ### Steering meetings
 
diff --git a/examples/rate-limiting/go.mod b/examples/rate-limiting/go.mod
new file mode 100644
index 0000000..5ec49dd
--- /dev/null
+++ b/examples/rate-limiting/go.mod
@@ -0,0 +1,8 @@
+module github.com/modelcontextprotocol/go-sdk/examples/rate-limiting
+
+go 1.25
+
+require (
+	github.com/modelcontextprotocol/go-sdk v0.0.0-20250625185707-09181c2c2e89
+	golang.org/x/time v0.12.0
+)
diff --git a/examples/rate-limiting/go.sum b/examples/rate-limiting/go.sum
new file mode 100644
index 0000000..c702768
--- /dev/null
+++ b/examples/rate-limiting/go.sum
@@ -0,0 +1,8 @@
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/modelcontextprotocol/go-sdk v0.0.0-20250625185707-09181c2c2e89 h1:kUGBYP25FTv3ZRBhLT4iQvtx4FDl7hPkWe3isYrMxyo=
+github.com/modelcontextprotocol/go-sdk v0.0.0-20250625185707-09181c2c2e89/go.mod h1:DcXfbr7yl7e35oMpzHfKw2nUYRjhIGS2uou/6tdsTB0=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
diff --git a/examples/rate-limiting/main.go b/examples/rate-limiting/main.go
new file mode 100644
index 0000000..7e91b79
--- /dev/null
+++ b/examples/rate-limiting/main.go
@@ -0,0 +1,54 @@
+// 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 main
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+	"golang.org/x/time/rate"
+)
+
+// GlobalRateLimiterMiddleware creates a middleware that applies a global rate limit.
+// Every request attempting to pass through will try to acquire a token.
+// If a token cannot be acquired immediately, the request will be rejected.
+func GlobalRateLimiterMiddleware[S mcp.Session](limiter *rate.Limiter) mcp.Middleware[S] {
+	return func(next mcp.MethodHandler[S]) mcp.MethodHandler[S] {
+		return func(ctx context.Context, session S, method string, params mcp.Params) (mcp.Result, error) {
+			if !limiter.Allow() {
+				return nil, errors.New("JSON RPC overloaded")
+			}
+			return next(ctx, session, method, params)
+		}
+	}
+}
+
+// PerMethodRateLimiterMiddleware creates a middleware that applies rate limiting
+// on a per-method basis.
+// Methods not specified in limiters will not be rate limited by this middleware.
+func PerMethodRateLimiterMiddleware[S mcp.Session](limiters map[string]*rate.Limiter) mcp.Middleware[S] {
+	return func(next mcp.MethodHandler[S]) mcp.MethodHandler[S] {
+		return func(ctx context.Context, session S, method string, params mcp.Params) (mcp.Result, error) {
+			if limiter, ok := limiters[method]; ok {
+				if !limiter.Allow() {
+					return nil, errors.New("JSON RPC overloaded")
+				}
+			}
+			return next(ctx, session, method, params)
+		}
+	}
+}
+
+func main() {
+	server := mcp.NewServer("greeter1", "v0.0.1", nil)
+	server.AddReceivingMiddleware(GlobalRateLimiterMiddleware[*mcp.ServerSession](rate.NewLimiter(rate.Every(time.Second/5), 10)))
+	server.AddReceivingMiddleware(PerMethodRateLimiterMiddleware[*mcp.ServerSession](map[string]*rate.Limiter{
+		"callTool":  rate.NewLimiter(rate.Every(time.Second), 5),  // once a second with a burst up to 5
+		"listTools": rate.NewLimiter(rate.Every(time.Minute), 20), // once a minute with a burst up to 20
+	}))
+	// Run Server logic.
+}
diff --git a/jsonschema/doc.go b/jsonschema/doc.go
index a53df32..f25b000 100644
--- a/jsonschema/doc.go
+++ b/jsonschema/doc.go
@@ -40,7 +40,7 @@ or as a value of this type:
 
 # Inference
 
-The [For] and [ForType] functions return a [Schema] describing the given Go type.
+The [For] function returns a [Schema] describing the given Go type.
 The type cannot contain any function or channel types, and any map types must have a string key.
 For example, calling For on the above Player type results in this schema:
 
diff --git a/jsonschema/infer.go b/jsonschema/infer.go
index 20ec134..1334bdf 100644
--- a/jsonschema/infer.go
+++ b/jsonschema/infer.go
@@ -15,40 +15,39 @@ import (
 
 // For constructs a JSON schema object for the given type argument.
 //
-// It is a convenience for ForType.
-func For[T any]() (*Schema, error) {
-	return ForType(reflect.TypeFor[T]())
-}
-
-// ForType constructs a JSON schema object for the given type.
 // It translates Go types into compatible JSON schema types, as follows:
-//   - strings have schema type "string"
-//   - bools have schema type "boolean"
-//   - signed and unsigned integer types have schema type "integer"
-//   - floating point types have schema type "number"
-//   - slices and arrays have schema type "array", and a corresponding schema
-//     for items
-//   - maps with string key have schema type "object", and corresponding
-//     schema for additionalProperties
-//   - structs have schema type "object", and disallow additionalProperties.
+//   - Strings have schema type "string".
+//   - Bools have schema type "boolean".
+//   - Signed and unsigned integer types have schema type "integer".
+//   - Floating point types have schema type "number".
+//   - Slices and arrays have schema type "array", and a corresponding schema
+//     for items.
+//   - Maps with string key have schema type "object", and corresponding
+//     schema for additionalProperties.
+//   - Structs have schema type "object", and disallow additionalProperties.
 //     Their properties are derived from exported struct fields, using the
-//     struct field json name. Fields that are marked "omitempty" are
+//     struct field JSON name. Fields that are marked "omitempty" are
 //     considered optional; all other fields become required properties.
 //
-// It returns an error if t contains (possibly recursively) any of the following Go
+// For returns an error if t contains (possibly recursively) any of the following Go
 // types, as they are incompatible with the JSON schema spec.
 //   - maps with key other than 'string'
 //   - function types
 //   - complex numbers
 //   - unsafe pointers
 //
-// The cannot be any cycles in the types.
-// TODO(rfindley): we could perhaps just skip these incompatible fields.
-func ForType(t reflect.Type) (*Schema, error) {
-	return typeSchema(t)
+// The types must not have cycles.
+func For[T any]() (*Schema, error) {
+	// TODO: consider skipping incompatible fields, instead of failing.
+	s, err := forType(reflect.TypeFor[T]())
+	if err != nil {
+		var z T
+		return nil, fmt.Errorf("For[%T](): %w", z, err)
+	}
+	return s, nil
 }
 
-func typeSchema(t reflect.Type) (*Schema, error) {
+func forType(t reflect.Type) (*Schema, error) {
 	// Follow pointers: the schema for *T is almost the same as for T, except that
 	// an explicit JSON "null" is allowed for the pointer.
 	allowNull := false
@@ -82,14 +81,14 @@ func typeSchema(t reflect.Type) (*Schema, error) {
 			return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
 		}
 		s.Type = "object"
-		s.AdditionalProperties, err = typeSchema(t.Elem())
+		s.AdditionalProperties, err = forType(t.Elem())
 		if err != nil {
 			return nil, fmt.Errorf("computing map value schema: %v", err)
 		}
 
 	case reflect.Slice, reflect.Array:
 		s.Type = "array"
-		s.Items, err = typeSchema(t.Elem())
+		s.Items, err = forType(t.Elem())
 		if err != nil {
 			return nil, fmt.Errorf("computing element schema: %v", err)
 		}
@@ -115,7 +114,7 @@ func typeSchema(t reflect.Type) (*Schema, error) {
 			if s.Properties == nil {
 				s.Properties = make(map[string]*Schema)
 			}
-			s.Properties[info.Name], err = typeSchema(field.Type)
+			s.Properties[info.Name], err = forType(field.Type)
 			if err != nil {
 				return nil, err
 			}
diff --git a/jsonschema/schema.go b/jsonschema/schema.go
index 1a73cc6..26623f1 100644
--- a/jsonschema/schema.go
+++ b/jsonschema/schema.go
@@ -127,6 +127,9 @@ type Schema struct {
 	// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
 	Format string `json:"format,omitempty"`
 
+	// Extra allows for additional keywords beyond those specified.
+	Extra map[string]any `json:"-"`
+
 	// computed fields
 
 	// This schema's base schema.
@@ -236,7 +239,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
 		Type:                 typ,
 		schemaWithoutMethods: (*schemaWithoutMethods)(s),
 	}
-	return json.Marshal(ms)
+	return marshalStructWithMap(&ms, "Extra")
 }
 
 func (s *Schema) UnmarshalJSON(data []byte) error {
@@ -269,7 +272,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
 	}{
 		schemaWithoutMethods: (*schemaWithoutMethods)(s),
 	}
-	if err := json.Unmarshal(data, &ms); err != nil {
+	if err := unmarshalStructWithMap(data, &ms, "Extra"); err != nil {
 		return err
 	}
 	// Unmarshal "type" as either Type or Types.
diff --git a/jsonschema/util.go b/jsonschema/util.go
index f884aff..71c3443 100644
--- a/jsonschema/util.go
+++ b/jsonschema/util.go
@@ -15,6 +15,9 @@ import (
 	"math/big"
 	"reflect"
 	"slices"
+	"sync"
+
+	"github.com/modelcontextprotocol/go-sdk/internal/util"
 )
 
 // Equal reports whether two Go values representing JSON values are equal according
@@ -282,3 +285,136 @@ func assert(cond bool, msg string) {
 		panic("assertion failed: " + msg)
 	}
 }
+
+// marshalStructWithMap marshals its first argument to JSON, treating the field named
+// mapField as an embedded map. The first argument must be a pointer to
+// a struct. The underlying type of mapField must be a map[string]any, and it must have
+// a "-" json tag, meaning it will not be marshaled.
+//
+// For example, given this struct:
+//
+//	type S struct {
+//	   A int
+//	   Extra map[string] any `json:"-"`
+//	}
+//
+// and this value:
+//
+//	s := S{A: 1, Extra: map[string]any{"B": 2}}
+//
+// the call marshalJSONWithMap(s, "Extra") would return
+//
+//	{"A": 1, "B": 2}
+//
+// It is an error if the map contains the same key as another struct field's
+// JSON name.
+//
+// marshalStructWithMap calls json.Marshal on a value of type T, so T must not
+// have a MarshalJSON method that calls this function, on pain of infinite regress.
+//
+// Note that there is a similar function in mcp/util.go, but they are not the same.
+// Here the function requires `-` json tag, does not clear the mapField map,
+// and handles embedded struct due to the implementation of jsonNames in this package.
+//
+// TODO: avoid this restriction on T by forcing it to marshal in a default way.
+// See https://go.dev/play/p/EgXKJHxEx_R.
+func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) {
+	// Marshal the struct and the map separately, and concatenate the bytes.
+	// This strategy is dramatically less complicated than
+	// constructing a synthetic struct or map with the combined keys.
+	if s == nil {
+		return []byte("null"), nil
+	}
+	s2 := *s
+	vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField)
+	mapVal := vMapField.Interface().(map[string]any)
+
+	// Check for duplicates.
+	names := jsonNames(reflect.TypeFor[T]())
+	for key := range mapVal {
+		if names[key] {
+			return nil, fmt.Errorf("map key %q duplicates struct field", key)
+		}
+	}
+
+	structBytes, err := json.Marshal(s2)
+	if err != nil {
+		return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err)
+	}
+	if len(mapVal) == 0 {
+		return structBytes, nil
+	}
+	mapBytes, err := json.Marshal(mapVal)
+	if err != nil {
+		return nil, err
+	}
+	if len(structBytes) == 2 { // must be "{}"
+		return mapBytes, nil
+	}
+	// "{X}" + "{Y}" => "{X,Y}"
+	res := append(structBytes[:len(structBytes)-1], ',')
+	res = append(res, mapBytes[1:]...)
+	return res, nil
+}
+
+// unmarshalStructWithMap is the inverse of marshalStructWithMap.
+// T has the same restrictions as in that function.
+//
+// Note that there is a similar function in mcp/util.go, but they are not the same.
+// Here jsonNames also returns fields from embedded structs, hence this function
+// handles embedded structs as well.
+func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error {
+	// Unmarshal into the struct, ignoring unknown fields.
+	if err := json.Unmarshal(data, v); err != nil {
+		return err
+	}
+	// Unmarshal into the map.
+	m := map[string]any{}
+	if err := json.Unmarshal(data, &m); err != nil {
+		return err
+	}
+	// Delete from the map the fields of the struct.
+	for n := range jsonNames(reflect.TypeFor[T]()) {
+		delete(m, n)
+	}
+	if len(m) != 0 {
+		reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m))
+	}
+	return nil
+}
+
+var jsonNamesMap sync.Map // from reflect.Type to map[string]bool
+
+// jsonNames returns the set of JSON object keys that t will marshal into,
+// including fields from embedded structs in t.
+// t must be a struct type.
+//
+// Note that there is a similar function in mcp/util.go, but they are not the same
+// Here the function recurses over embedded structs and includes fields from them.
+func jsonNames(t reflect.Type) map[string]bool {
+	// Lock not necessary: at worst we'll duplicate work.
+	if val, ok := jsonNamesMap.Load(t); ok {
+		return val.(map[string]bool)
+	}
+	m := map[string]bool{}
+	for i := range t.NumField() {
+		field := t.Field(i)
+		// handle embedded structs
+		if field.Anonymous {
+			fieldType := field.Type
+			if fieldType.Kind() == reflect.Ptr {
+				fieldType = fieldType.Elem()
+			}
+			for n := range jsonNames(fieldType) {
+				m[n] = true
+			}
+			continue
+		}
+		info := util.FieldJSONInfo(field)
+		if !info.Omit {
+			m[info.Name] = true
+		}
+	}
+	jsonNamesMap.Store(t, m)
+	return m
+}
diff --git a/mcp/client.go b/mcp/client.go
index 82fbb01..856df56 100644
--- a/mcp/client.go
+++ b/mcp/client.go
@@ -191,7 +191,7 @@ func (c *Client) RemoveRoots(uris ...string) {
 		func() bool { return c.roots.remove(uris...) })
 }
 
-// changeAndNotifyClient is called when a feature is added or removed.
+// changeAndNotify is called when a feature is added or removed.
 // It calls change, which should do the work and report whether a change actually occurred.
 // If there was a change, it notifies a snapshot of the sessions.
 func (c *Client) changeAndNotify(notification string, params Params, change func() bool) {
@@ -347,7 +347,7 @@ func (cs *ClientSession) ListResourceTemplates(ctx context.Context, params *List
 	return handleSend[*ListResourceTemplatesResult](ctx, cs, methodListResourceTemplates, orZero[Params](params))
 }
 
-// ReadResource ask the server to read a resource and return its contents.
+// ReadResource asks the server to read a resource and return its contents.
 func (cs *ClientSession) ReadResource(ctx context.Context, params *ReadResourceParams) (*ReadResourceResult, error) {
 	return handleSend[*ReadResourceResult](ctx, cs, methodReadResource, orZero[Params](params))
 }
diff --git a/mcp/logging.go b/mcp/logging.go
index 65ebe06..4880e17 100644
--- a/mcp/logging.go
+++ b/mcp/logging.go
@@ -137,7 +137,7 @@ func (h *LoggingHandler) WithGroup(name string) slog.Handler {
 }
 
 // Handle implements [slog.Handler.Handle] by writing the Record to a JSONHandler,
-// then calling [ServerSession.LoggingMesssage] with the result.
+// 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.
diff --git a/mcp/protocol.go b/mcp/protocol.go
index babfd55..73e133a 100644
--- a/mcp/protocol.go
+++ b/mcp/protocol.go
@@ -39,70 +39,18 @@ type Annotations struct {
 	Priority float64 `json:"priority,omitempty"`
 }
 
-type CallToolParams struct {
-	// This property is reserved by the protocol to allow clients and servers to
-	// attach additional metadata to their responses.
-	Meta      `json:"_meta,omitempty"`
-	Arguments any    `json:"arguments,omitempty"`
-	Name      string `json:"name"`
-}
-
-func (x *CallToolParams) GetProgressToken() any  { return getProgressToken(x) }
-func (x *CallToolParams) SetProgressToken(t any) { setProgressToken(x, t) }
+type CallToolParams = CallToolParamsFor[any]
 
 type CallToolParamsFor[In any] struct {
 	// This property is reserved by the protocol to allow clients and servers to
 	// attach additional metadata to their responses.
 	Meta      `json:"_meta,omitempty"`
-	Arguments In     `json:"arguments,omitempty"`
 	Name      string `json:"name"`
+	Arguments In     `json:"arguments,omitempty"`
 }
 
 // The server's response to a tool call.
-type CallToolResult struct {
-	// This property is reserved by the protocol to allow clients and servers to
-	// attach additional metadata to their responses.
-	Meta `json:"_meta,omitempty"`
-	// A list of content objects that represent the unstructured result of the tool
-	// call.
-	Content []Content `json:"content"`
-	// Whether the tool call ended in an error.
-	//
-	// If not set, this is assumed to be false (the call was successful).
-	//
-	// Any errors that originate from the tool should be reported inside the result
-	// object, with isError set to true, not as an MCP protocol-level error
-	// response. Otherwise, the LLM would not be able to see that an error occurred
-	// and self-correct.
-	//
-	// However, any errors in finding the tool, an error indicating that the
-	// server does not support tool calls, or any other exceptional conditions,
-	// should be reported as an MCP error response.
-	IsError bool `json:"isError,omitempty"`
-	// An optional JSON object that represents the structured result of the tool
-	// call.
-	// TODO(jba,rfindley): should this be any?
-	StructuredContent map[string]any `json:"structuredContent,omitempty"`
-}
-
-// UnmarshalJSON handles the unmarshalling of content into the Content
-// interface.
-func (x *CallToolResult) UnmarshalJSON(data []byte) error {
-	type res CallToolResult // avoid recursion
-	var wire struct {
-		res
-		Content []*wireContent `json:"content"`
-	}
-	if err := json.Unmarshal(data, &wire); err != nil {
-		return err
-	}
-	var err error
-	if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
-		return err
-	}
-	*x = CallToolResult(wire.res)
-	return nil
-}
+type CallToolResult = CallToolResultFor[any]
 
 type CallToolResultFor[Out any] struct {
 	// This property is reserved by the protocol to allow clients and servers to
@@ -111,6 +59,9 @@ type CallToolResultFor[Out any] struct {
 	// A list of content objects that represent the unstructured result of the tool
 	// call.
 	Content []Content `json:"content"`
+	// An optional JSON object that represents the structured result of the tool
+	// call.
+	StructuredContent Out `json:"structuredContent,omitempty"`
 	// Whether the tool call ended in an error.
 	//
 	// If not set, this is assumed to be false (the call was successful).
@@ -124,9 +75,6 @@ type CallToolResultFor[Out any] struct {
 	// server does not support tool calls, or any other exceptional conditions,
 	// should be reported as an MCP error response.
 	IsError bool `json:"isError,omitempty"`
-	// An optional JSON object that represents the structured result of the tool
-	// call.
-	StructuredContent Out `json:"structuredContent,omitempty"`
 }
 
 // UnmarshalJSON handles the unmarshalling of content into the Content
@@ -148,6 +96,9 @@ func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
+func (x *CallToolParamsFor[Out]) GetProgressToken() any  { return getProgressToken(x) }
+func (x *CallToolParamsFor[Out]) SetProgressToken(t any) { setProgressToken(x, t) }
+
 type CancelledParams struct {
 	// This property is reserved by the protocol to allow clients and servers to
 	// attach additional metadata to their responses.
diff --git a/mcp/streamable.go b/mcp/streamable.go
index 63211c2..a1952f7 100644
--- a/mcp/streamable.go
+++ b/mcp/streamable.go
@@ -254,7 +254,7 @@ func (s *StreamableServerTransport) Connect(context.Context) (Connection, error)
 //
 // Currently, this is implemented in [ServerSession.handle]. This is not ideal,
 // because it means that a user of the MCP package couldn't implement the
-// streamable transport, as they'd lack this priviledged access.
+// streamable transport, as they'd lack this privileged access.
 //
 // If we ever wanted to expose this mechanism, we have a few options:
 //  1. Make ServerSession an interface, and provide an implementation of
diff --git a/mcp/tool.go b/mcp/tool.go
index 5bf9440..a6f228e 100644
--- a/mcp/tool.go
+++ b/mcp/tool.go
@@ -17,7 +17,7 @@ import (
 // A ToolHandler handles a call to tools/call.
 // [CallToolParams.Arguments] will contain a map[string]any that has been validated
 // against the input schema.
-// Perhaps this should be an alias for ToolHandlerFor[map[string]any, map[string]any].
+// TODO: Perhaps this should be an alias for ToolHandlerFor[map[string]any, map[string]any]?
 type ToolHandler func(context.Context, *ServerSession, *CallToolParamsFor[map[string]any]) (*CallToolResult, error)
 
 // A ToolHandlerFor handles a call to tools/call with typed arguments and results.



Test patch:
diff --git a/jsonschema/schema_test.go b/jsonschema/schema_test.go
index 0d3ea09..4ceb1ee 100644
--- a/jsonschema/schema_test.go
+++ b/jsonschema/schema_test.go
@@ -26,6 +26,7 @@ func TestGoRoundTrip(t *testing.T) {
 		{Const: Ptr(any(map[string]any{}))},
 		{Default: mustMarshal(1)},
 		{Default: mustMarshal(nil)},
+		{Extra: map[string]any{"test": "value"}},
 	} {
 		data, err := json.Marshal(s)
 		if err != nil {
@@ -64,11 +65,19 @@ func TestJSONRoundTrip(t *testing.T) {
 			`{"$vocabulary":{"b":true, "a":false}}`,
 			`{"$vocabulary":{"a":false,"b":true}}`,
 		},
-		{`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately
+		{`{"unk":0}`, `{"unk":0}`}, // unknown fields are not dropped
+		{
+			// known and unknown fields are not dropped
+			// note that the order will be by the declaration order in the anonymous struct inside MarshalJSON
+			`{"comment":"test","type":"example","unk":0}`,
+			`{"type":"example","comment":"test","unk":0}`,
+		},
+		{`{"extra":0}`, `{"extra":0}`}, // extra is not a special keyword and should not be dropped
+		{`{"Extra":0}`, `{"Extra":0}`}, // Extra is not a special keyword and should not be dropped
 	} {
 		var s Schema
 		mustUnmarshal(t, []byte(tt.in), &s)
-		data, err := json.Marshal(s)
+		data, err := json.Marshal(&s)
 		if err != nil {
 			t.Fatal(err)
 		}
diff --git a/jsonschema/util_test.go b/jsonschema/util_test.go
index 189cb26..03ccb4d 100644
--- a/jsonschema/util_test.go
+++ b/jsonschema/util_test.go
@@ -8,7 +8,11 @@ import (
 	"encoding/json"
 	"hash/maphash"
 	"reflect"
+	"strings"
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
 func TestEqual(t *testing.T) {
@@ -125,3 +129,58 @@ func TestHash(t *testing.T) {
 	}
 	_ = hash(null)
 }
+
+func TestMarshalStructWithMap(t *testing.T) {
+	type S struct {
+		A int
+		B string `json:"b,omitempty"`
+		u bool
+		M map[string]any `json:"-"`
+	}
+	t.Run("basic", func(t *testing.T) {
+		s := S{A: 1, B: "two", M: map[string]any{"!@#": true}}
+		got, err := marshalStructWithMap(&s, "M")
+		if err != nil {
+			t.Fatal(err)
+		}
+		want := `{"A":1,"b":"two","!@#":true}`
+		if g := string(got); g != want {
+			t.Errorf("\ngot  %s\nwant %s", g, want)
+		}
+
+		var un S
+		if err := unmarshalStructWithMap(got, &un, "M"); err != nil {
+			t.Fatal(err)
+		}
+		if diff := cmp.Diff(s, un, cmpopts.IgnoreUnexported(S{})); diff != "" {
+			t.Errorf("mismatch (-want, +got):\n%s", diff)
+		}
+	})
+	t.Run("duplicate", func(t *testing.T) {
+		s := S{A: 1, B: "two", M: map[string]any{"b": "dup"}}
+		_, err := marshalStructWithMap(&s, "M")
+		if err == nil || !strings.Contains(err.Error(), "duplicate") {
+			t.Errorf("got %v, want error with 'duplicate'", err)
+		}
+	})
+	t.Run("embedded", func(t *testing.T) {
+		type Embedded struct {
+			A     int
+			B     int
+			Extra map[string]any `json:"-"`
+		}
+		type S struct {
+			C int
+			Embedded
+		}
+		s := S{C: 1, Embedded: Embedded{A: 2, B: 3, Extra: map[string]any{"d": 4, "e": 5}}}
+		got, err := marshalStructWithMap(&s, "Extra")
+		if err != nil {
+			t.Fatal(err)
+		}
+		want := `{"C":1,"A":2,"B":3,"d":4,"e":5}`
+		if g := string(got); g != want {
+			t.Errorf("got %v, want %v", g, want)
+		}
+	})
+}
diff --git a/mcp/content_test.go b/mcp/content_test.go
index 5270f99..5e6c6d0 100644
--- a/mcp/content_test.go
+++ b/mcp/content_test.go
@@ -7,7 +7,6 @@ package mcp_test
 import (
 	"encoding/json"
 	"fmt"
-	"log"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -94,7 +93,6 @@ func TestContent(t *testing.T) {
 			t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff)
 		}
 		result := fmt.Sprintf(`{"content":[%s]}`, string(got))
-		log.Println(result)
 		var out mcp.CallToolResult
 		if err := json.Unmarshal([]byte(result), &out); err != nil {
 			t.Fatal(err)