Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4608cda997 |
||
|
|
3b917b466c |
||
|
|
b58aa8d8d8 |
||
|
|
826e653c27 |
||
|
|
3785c500b5 |
||
|
|
fbd36cb787 |
||
|
|
c1918e19ad |
||
|
|
0c0eb23625 |
||
|
|
3f43bcf012 |
||
|
|
a82c86a6cd |
||
|
|
3210c2b34c |
||
|
|
e40f35d137 |
||
|
|
5bc078a795 |
||
|
|
f99cdb649b |
||
|
|
a5026ea7b7 |
28 changed files with 1459 additions and 139 deletions
|
|
@ -44,7 +44,12 @@ func authorizationServerMetadataURLs(issuerURL string) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseURL.Path == "" {
|
// RFC 8414, section 3.1 requires any terminating "/" to be removed from the
|
||||||
|
// issuer identifier before the well-known suffix is inserted, so an issuer
|
||||||
|
// such as "https://auth.example.com/" has no path component at all.
|
||||||
|
issuerPath := strings.Trim(baseURL.Path, "/")
|
||||||
|
|
||||||
|
if issuerPath == "" {
|
||||||
// "OAuth 2.0 Authorization Server Metadata".
|
// "OAuth 2.0 Authorization Server Metadata".
|
||||||
baseURL.Path = "/.well-known/oauth-authorization-server"
|
baseURL.Path = "/.well-known/oauth-authorization-server"
|
||||||
urls = append(urls, baseURL.String())
|
urls = append(urls, baseURL.String())
|
||||||
|
|
@ -54,15 +59,14 @@ func authorizationServerMetadataURLs(issuerURL string) []string {
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
originalPath := baseURL.Path
|
|
||||||
// "OAuth 2.0 Authorization Server Metadata with path insertion".
|
// "OAuth 2.0 Authorization Server Metadata with path insertion".
|
||||||
baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/")
|
baseURL.Path = "/.well-known/oauth-authorization-server/" + issuerPath
|
||||||
urls = append(urls, baseURL.String())
|
urls = append(urls, baseURL.String())
|
||||||
// "OpenID Connect Discovery 1.0 with path insertion".
|
// "OpenID Connect Discovery 1.0 with path insertion".
|
||||||
baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/")
|
baseURL.Path = "/.well-known/openid-configuration/" + issuerPath
|
||||||
urls = append(urls, baseURL.String())
|
urls = append(urls, baseURL.String())
|
||||||
// "OpenID Connect Discovery 1.0 with path appending".
|
// "OpenID Connect Discovery 1.0 with path appending".
|
||||||
baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration"
|
baseURL.Path = "/" + issuerPath + "/.well-known/openid-configuration"
|
||||||
urls = append(urls, baseURL.String())
|
urls = append(urls, baseURL.String())
|
||||||
|
|
||||||
return urls
|
return urls
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,81 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/internal/oauthtest"
|
"github.com/modelcontextprotocol/go-sdk/internal/oauthtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAuthorizationServerMetadataURLs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
issuerURL string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
issuerURL: "https://auth.example.com",
|
||||||
|
want: []string{
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server",
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuerURL: "https://auth.example.com/",
|
||||||
|
want: []string{
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server",
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuerURL: "https://auth.example.com/tenant1",
|
||||||
|
want: []string{
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1",
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration/tenant1",
|
||||||
|
"https://auth.example.com/tenant1/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuerURL: "https://auth.example.com/tenant1/",
|
||||||
|
want: []string{
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1",
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration/tenant1",
|
||||||
|
"https://auth.example.com/tenant1/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.issuerURL, func(t *testing.T) {
|
||||||
|
if got := authorizationServerMetadataURLs(tt.issuerURL); !slices.Equal(got, tt.want) {
|
||||||
|
t.Errorf("authorizationServerMetadataURLs(%q) =\n%v\nwant\n%v", tt.issuerURL, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetAuthServerMetadataTrailingSlashIssuer checks that an issuer identifier
|
||||||
|
// written with a terminating slash still finds metadata served at the root
|
||||||
|
// well-known path.
|
||||||
|
func TestGetAuthServerMetadataTrailingSlashIssuer(t *testing.T) {
|
||||||
|
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
|
||||||
|
MetadataEndpointConfig: &oauthtest.MetadataEndpointConfig{
|
||||||
|
ServeOAuthInsertedEndpoint: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
s.Start(t)
|
||||||
|
|
||||||
|
got, err := GetAuthServerMetadata(t.Context(), s.URL()+"/", http.DefaultClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAuthServerMetadata() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("GetAuthServerMetadata() got nil, want metadata")
|
||||||
|
}
|
||||||
|
if got.Issuer != s.URL() {
|
||||||
|
t.Errorf("GetAuthServerMetadata() issuer = %q, want %q", got.Issuer, s.URL())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetAuthServerMetadata(t *testing.T) {
|
func TestGetAuthServerMetadata(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ func Example_elicitation() {
|
||||||
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
||||||
client renders as a form. Two field keywords shape that form.
|
client renders as a form. Two field keywords shape that form.
|
||||||
|
|
||||||
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034)) prefills a
|
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034--support-default-values-for-all-primitive-types-in)) prefills a
|
||||||
field. When the user accepts without supplying it, the SDK fills the field in
|
field. When the user accepts without supplying it, the SDK fills the field in
|
||||||
from the schema before the result reaches either side's caller — the client
|
from the schema before the result reaches either side's caller — the client
|
||||||
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
||||||
|
|
@ -313,7 +313,7 @@ defaulted field `Required` defeats it: accepted content is validated against
|
||||||
the schema before defaults are applied, so an answer that omits the field is
|
the schema before defaults are applied, so an answer that omits the field is
|
||||||
rejected rather than defaulted.
|
rejected rather than defaulted.
|
||||||
|
|
||||||
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330)) restricts a
|
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330-elicitation-enum-schema-improvements-and-standards)) restricts a
|
||||||
field to a fixed set of values, which the client renders as a choice. Enums
|
field to a fixed set of values, which the client renders as a choice. Enums
|
||||||
are supported only on `"string"` fields; declaring one on another type is
|
are supported only on `"string"` fields; declaring one on another type is
|
||||||
rejected. To label the choices, set the legacy `enumNames` keyword through
|
rejected. To label the choices, set the legacy `enumNames` keyword through
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,16 @@
|
||||||
|
|
||||||
## `MCPGODEBUG` history
|
## `MCPGODEBUG` history
|
||||||
|
|
||||||
|
### 1.9.0
|
||||||
|
|
||||||
|
Options listed below were added and will be removed in the 1.11.0 version of the SDK.
|
||||||
|
|
||||||
|
- `structuredcontentfloat64` added. If set to `1`, numbers in
|
||||||
|
`CallToolResult.StructuredContent` are decoded as `float64`, restoring the
|
||||||
|
previous behavior. The default behavior decodes numbers as `json.Number` so
|
||||||
|
that large integers and other exact JSON values do not silently lose
|
||||||
|
precision.
|
||||||
|
|
||||||
### 1.8.0
|
### 1.8.0
|
||||||
|
|
||||||
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
|
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
|
||||||
|
|
|
||||||
|
|
@ -1066,8 +1066,12 @@ server produces logs that remain server-side, for use by server maintainers.)
|
||||||
**Server-side**:
|
**Server-side**:
|
||||||
The minimum log level is part of the server state.
|
The minimum log level is part of the server state.
|
||||||
For stateful sessions, there is no default log level: no log messages will be sent
|
For stateful sessions, there is no default log level: no log messages will be sent
|
||||||
until the client calls `SetLevel` (see below).
|
until the client calls `SetLoggingLevel` (see below).
|
||||||
For stateful sessions, the level defaults to "info".
|
For legacy stateless sessions the level defaults to "info".
|
||||||
|
For sessionless 2026-07-28 requests there is no server-side default: each request
|
||||||
|
carries its level in `_meta` (`io.modelcontextprotocol/logLevel`), and nothing is
|
||||||
|
logged until the client sends one.
|
||||||
|
(Note: logging is deprecated as of 2026-07-28 / SEP-2577.)
|
||||||
|
|
||||||
[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients.
|
[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients.
|
||||||
It sends a logging notification to the client if the level of the message
|
It sends a logging notification to the client if the level of the message
|
||||||
|
|
@ -1083,7 +1087,7 @@ Servers always report the logging capability.
|
||||||
**Client-side**:
|
**Client-side**:
|
||||||
Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages.
|
Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages.
|
||||||
|
|
||||||
Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session.
|
Call [`ClientSession.SetLoggingLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLoggingLevel) to change the log level for a session.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func Example_logging() {
|
func Example_logging() {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func main() {
|
||||||
logger.Info("MCP method started",
|
logger.Info("MCP method started",
|
||||||
"method", method,
|
"method", method,
|
||||||
"session_id", req.GetSession().ID(),
|
"session_id", req.GetSession().ID(),
|
||||||
"has_params", req.GetParams() != nil,
|
"has_params", mcp.HasParams(req),
|
||||||
)
|
)
|
||||||
// Log more for tool calls.
|
// Log more for tool calls.
|
||||||
if ctr, ok := req.(*mcp.CallToolRequest); ok {
|
if ctr, ok := req.(*mcp.CallToolRequest); ok {
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ pattern.
|
||||||
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
`ElicitParams.RequestedSchema` is a flat schema of primitive fields, which the
|
||||||
client renders as a form. Two field keywords shape that form.
|
client renders as a form. Two field keywords shape that form.
|
||||||
|
|
||||||
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034)) prefills a
|
A `Default` ([SEP-1034](https://modelcontextprotocol.io/seps/1034--support-default-values-for-all-primitive-types-in)) prefills a
|
||||||
field. When the user accepts without supplying it, the SDK fills the field in
|
field. When the user accepts without supplying it, the SDK fills the field in
|
||||||
from the schema before the result reaches either side's caller — the client
|
from the schema before the result reaches either side's caller — the client
|
||||||
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
does so after its elicitation handler returns, and `ServerSession.Elicit` does
|
||||||
|
|
@ -108,7 +108,7 @@ defaulted field `Required` defeats it: accepted content is validated against
|
||||||
the schema before defaults are applied, so an answer that omits the field is
|
the schema before defaults are applied, so an answer that omits the field is
|
||||||
rejected rather than defaulted.
|
rejected rather than defaulted.
|
||||||
|
|
||||||
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330)) restricts a
|
An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330-elicitation-enum-schema-improvements-and-standards)) restricts a
|
||||||
field to a fixed set of values, which the client renders as a choice. Enums
|
field to a fixed set of values, which the client renders as a choice. Enums
|
||||||
are supported only on `"string"` fields; declaring one on another type is
|
are supported only on `"string"` fields; declaring one on another type is
|
||||||
rejected. To label the choices, set the legacy `enumNames` keyword through
|
rejected. To label the choices, set the legacy `enumNames` keyword through
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@
|
||||||
|
|
||||||
## `MCPGODEBUG` history
|
## `MCPGODEBUG` history
|
||||||
|
|
||||||
|
### 1.9.0
|
||||||
|
|
||||||
|
Options listed below were added and will be removed in the 1.11.0 version of the SDK.
|
||||||
|
|
||||||
|
- `structuredcontentfloat64` added. If set to `1`, numbers in
|
||||||
|
`CallToolResult.StructuredContent` are decoded as `float64`, restoring the
|
||||||
|
previous behavior. The default behavior decodes numbers as `json.Number` so
|
||||||
|
that large integers and other exact JSON values do not silently lose
|
||||||
|
precision.
|
||||||
|
|
||||||
### 1.8.0
|
### 1.8.0
|
||||||
|
|
||||||
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
|
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
|
||||||
|
|
|
||||||
|
|
@ -437,8 +437,12 @@ server produces logs that remain server-side, for use by server maintainers.)
|
||||||
**Server-side**:
|
**Server-side**:
|
||||||
The minimum log level is part of the server state.
|
The minimum log level is part of the server state.
|
||||||
For stateful sessions, there is no default log level: no log messages will be sent
|
For stateful sessions, there is no default log level: no log messages will be sent
|
||||||
until the client calls `SetLevel` (see below).
|
until the client calls `SetLoggingLevel` (see below).
|
||||||
For stateful sessions, the level defaults to "info".
|
For legacy stateless sessions the level defaults to "info".
|
||||||
|
For sessionless 2026-07-28 requests there is no server-side default: each request
|
||||||
|
carries its level in `_meta` (`io.modelcontextprotocol/logLevel`), and nothing is
|
||||||
|
logged until the client sends one.
|
||||||
|
(Note: logging is deprecated as of 2026-07-28 / SEP-2577.)
|
||||||
|
|
||||||
[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients.
|
[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients.
|
||||||
It sends a logging notification to the client if the level of the message
|
It sends a logging notification to the client if the level of the message
|
||||||
|
|
@ -454,7 +458,7 @@ Servers always report the logging capability.
|
||||||
**Client-side**:
|
**Client-side**:
|
||||||
Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages.
|
Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages.
|
||||||
|
|
||||||
Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session.
|
Call [`ClientSession.SetLoggingLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLoggingLevel) to change the log level for a session.
|
||||||
|
|
||||||
%include ../../mcp/server_example_test.go logging -
|
%include ../../mcp/server_example_test.go logging -
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ func Unmarshal(data []byte, v any) error {
|
||||||
return NewDecoder(bytes.NewReader(data)).Decode(v)
|
return NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UnmarshalUseNumber(data []byte, v any) error {
|
||||||
|
if err := checkMaxDepth(data, defaultMaxDepth); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dec := NewDecoder(bytes.NewReader(data))
|
||||||
|
dec.dec.UseNumber()
|
||||||
|
return dec.Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
// checkMaxDepth scans data once and reports [errMaxDepthExceeded] if the
|
// checkMaxDepth scans data once and reports [errMaxDepthExceeded] if the
|
||||||
// nesting of JSON objects and arrays exceeds maxDepth. It is a lightweight pass which
|
// nesting of JSON objects and arrays exceeds maxDepth. It is a lightweight pass which
|
||||||
// tracks '{' and '[' against '}' and ']' while skipping over the contents of strings.
|
// tracks '{' and '[' against '}' and ']' while skipping over the contents of strings.
|
||||||
|
|
|
||||||
|
|
@ -453,12 +453,19 @@ func (ac *AsyncCall) Await(ctx context.Context, result any) error {
|
||||||
// will not cause any messages that have not arrived yet with that ID to be
|
// will not cause any messages that have not arrived yet with that ID to be
|
||||||
// cancelled.
|
// cancelled.
|
||||||
func (c *Connection) Cancel(id ID) {
|
func (c *Connection) Cancel(id ID) {
|
||||||
|
c.CancelCause(id, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelCause is like [Connection.Cancel], but records cause as the reason
|
||||||
|
// the Context was cancelled, so that the Handle call can read it back through
|
||||||
|
// [context.Cause]. A nil cause reads as [context.Canceled].
|
||||||
|
func (c *Connection) CancelCause(id ID, cause error) {
|
||||||
var req *incomingRequest
|
var req *incomingRequest
|
||||||
c.updateInFlight(func(s *inFlightState) {
|
c.updateInFlight(func(s *inFlightState) {
|
||||||
req = s.incomingByID[id]
|
req = s.incomingByID[id]
|
||||||
})
|
})
|
||||||
if req != nil {
|
if req != nil {
|
||||||
req.cancel(nil)
|
req.cancel(cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1147,6 +1147,9 @@ func (c *Client) AddSendingMiddleware(middleware ...Middleware) {
|
||||||
//
|
//
|
||||||
// Receiving middleware is called when a request is received. It is useful for tasks
|
// Receiving middleware is called when a request is received. It is useful for tasks
|
||||||
// such as authentication, request logging and metrics.
|
// such as authentication, request logging and metrics.
|
||||||
|
//
|
||||||
|
// A received message need not carry params: use [HasParams] before inspecting
|
||||||
|
// [Request.GetParams].
|
||||||
func (c *Client) AddReceivingMiddleware(middleware ...Middleware) {
|
func (c *Client) AddReceivingMiddleware(middleware ...Middleware) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
|
||||||
224
mcp/mcp_test.go
224
mcp/mcp_test.go
|
|
@ -710,13 +710,13 @@ func TestCancellation(t *testing.T) {
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
start = make(chan struct{})
|
start = make(chan struct{})
|
||||||
cancelled = make(chan struct{}, 1) // don't block the request
|
cancelled = make(chan error, 1) // don't block the request
|
||||||
)
|
)
|
||||||
slowTool := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) {
|
slowTool := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) {
|
||||||
start <- struct{}{}
|
start <- struct{}{}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
cancelled <- struct{}{}
|
cancelled <- context.Cause(ctx)
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -732,7 +732,72 @@ func TestCancellation(t *testing.T) {
|
||||||
<-start
|
<-start
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
<-cancelled
|
// The client sends its context's error as the reason of its cancelled
|
||||||
|
// notification, and the handler reads it back as the cause.
|
||||||
|
cause := <-cancelled
|
||||||
|
if !errors.Is(cause, context.Canceled) {
|
||||||
|
t.Errorf("context.Cause = %v, want it to wrap context.Canceled", cause)
|
||||||
|
}
|
||||||
|
if want := "request cancelled by the peer: " + context.Canceled.Error(); cause == nil || cause.Error() != want {
|
||||||
|
t.Errorf("context.Cause = %v, want %q", cause, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCancellationReason verifies that the reason a peer gives in its
|
||||||
|
// cancelled notification reaches the handler as the cause of its context,
|
||||||
|
// which is what lets a server log it as the specification asks.
|
||||||
|
//
|
||||||
|
// The call and the notification are written to the connection directly, so
|
||||||
|
// the reason is one the SDK's own client would never send; the session is
|
||||||
|
// pinned to 2025-11-25 so that a request written that way needs no _meta.
|
||||||
|
func TestCancellationReason(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
start = make(chan struct{})
|
||||||
|
cancelled = make(chan error, 1) // don't block the request
|
||||||
|
)
|
||||||
|
slowTool := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) {
|
||||||
|
start <- struct{}{}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
cancelled <- context.Cause(ctx)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
cancelled <- nil
|
||||||
|
}
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
ct, st := NewInMemoryTransports()
|
||||||
|
s := NewServer(testImpl, nil)
|
||||||
|
AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{Type: "object"}}, slowTool)
|
||||||
|
ss, err := s.Connect(ctx, st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = ss.Close() })
|
||||||
|
cs, err := NewClient(testImpl, nil).Connect(ctx, ct, &ClientSessionOptions{ProtocolVersion: protocolVersion20251125})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cs.Close() })
|
||||||
|
|
||||||
|
call := cs.conn.Call(ctx, methodCallTool, &CallToolParams{Name: "slow"})
|
||||||
|
<-start
|
||||||
|
if err := cs.conn.Notify(ctx, notificationCancelled, &CancelledParams{RequestID: call.ID().Raw(), Reason: "user asked"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cause := <-cancelled
|
||||||
|
if cause == nil {
|
||||||
|
t.Fatal("the tool ran to completion, want it cancelled")
|
||||||
|
}
|
||||||
|
if !errors.Is(cause, context.Canceled) {
|
||||||
|
t.Errorf("context.Cause = %v, want it to wrap context.Canceled", cause)
|
||||||
|
}
|
||||||
|
if got, want := cause.Error(), "request cancelled by the peer: user asked"; got != want {
|
||||||
|
t.Errorf("context.Cause = %q, want %q", got, want)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3298,6 +3363,159 @@ func TestSubscriptionsListen_DisconnectScrubsMaps(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newSubListenSubscribeServer is newSubListenServer with a resource and the
|
||||||
|
// handlers that make the server advertise resources.subscribe, so that a
|
||||||
|
// listen can carry a resource subscription.
|
||||||
|
func newSubListenSubscribeServer() *Server {
|
||||||
|
s := NewServer(testImpl, &ServerOptions{
|
||||||
|
SubscribeHandler: func(context.Context, *SubscribeRequest) error { return nil },
|
||||||
|
UnsubscribeHandler: func(context.Context, *UnsubscribeRequest) error { return nil },
|
||||||
|
})
|
||||||
|
AddTool(s, &Tool{Name: "t1"}, sayHi)
|
||||||
|
s.AddPrompt(&Prompt{Name: "p1"}, nil)
|
||||||
|
s.AddResource(&Resource{Name: "r1", URI: "file:///r1"}, nil)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitSubListenEvent(t *testing.T, events chan subListenEvent, kind string) subListenEvent {
|
||||||
|
t.Helper()
|
||||||
|
select {
|
||||||
|
case e := <-events:
|
||||||
|
if e.kind != kind {
|
||||||
|
t.Fatalf("got event %q, want %q", e.kind, kind)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("timed out waiting for %q", kind)
|
||||||
|
return subListenEvent{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSubscriptionsListen_TeardownKeepsOtherListens verifies that unwinding
|
||||||
|
// one listen leaves the other listens on the same session registered.
|
||||||
|
// Unsubscribe tears down the listen that Subscribe opened for the resource;
|
||||||
|
// the auto-listen opened by Connect must keep delivering list-changed
|
||||||
|
// notifications.
|
||||||
|
func TestSubscriptionsListen_TeardownKeepsOtherListens(t *testing.T) {
|
||||||
|
events := make(chan subListenEvent, 16)
|
||||||
|
server := newSubListenSubscribeServer()
|
||||||
|
|
||||||
|
ct, st := NewInMemoryTransports()
|
||||||
|
ss, err := server.Connect(context.Background(), st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("server connect: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cs, err := newSubListenClient(events).Connect(ctx, ct,
|
||||||
|
&ClientSessionOptions{ProtocolVersion: protocolVersion20260728})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client connect: %v", err)
|
||||||
|
}
|
||||||
|
defer cs.Close()
|
||||||
|
|
||||||
|
autoListen := waitSubListenEvent(t, events, "ack")
|
||||||
|
|
||||||
|
if err := cs.Subscribe(ctx, &SubscribeParams{URI: "file:///r1"}); err != nil {
|
||||||
|
t.Fatalf("Subscribe: %v", err)
|
||||||
|
}
|
||||||
|
resourceListen := waitSubListenEvent(t, events, "ack")
|
||||||
|
if resourceListen.id == autoListen.id {
|
||||||
|
t.Fatalf("Subscribe reused subscription ID %s", autoListen.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cs.Unsubscribe(ctx, &UnsubscribeParams{URI: "file:///r1"}); err != nil {
|
||||||
|
t.Fatalf("Unsubscribe: %v", err)
|
||||||
|
}
|
||||||
|
// Wait for the server to unwind that listen, observed through a registry
|
||||||
|
// the auto-listen does not appear in.
|
||||||
|
waitUntil(t, 5*time.Second, "resource listen to unwind", func() bool {
|
||||||
|
server.mu.Lock()
|
||||||
|
defer server.mu.Unlock()
|
||||||
|
_, ok := server.resourceSubscriptions["file:///r1"]
|
||||||
|
return !ok
|
||||||
|
})
|
||||||
|
|
||||||
|
AddTool(server, &Tool{Name: "t2"}, sayHi)
|
||||||
|
if got := waitSubListenEvent(t, events, "tool"); got.id != autoListen.id {
|
||||||
|
t.Errorf("tool notification id = %s, want auto-listen %s", got.id, autoListen.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSubscriptionsListen_TeardownRetiresOwnRegistration verifies that a
|
||||||
|
// listen still retires what it registered, so that per-listen tracking does
|
||||||
|
// not turn into a leak.
|
||||||
|
func TestSubscriptionsListen_TeardownRetiresOwnRegistration(t *testing.T) {
|
||||||
|
events := make(chan subListenEvent, 8)
|
||||||
|
server := newSubListenServer()
|
||||||
|
|
||||||
|
ct, st := NewInMemoryTransports()
|
||||||
|
ss, err := server.Connect(context.Background(), st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("server connect: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Close()
|
||||||
|
|
||||||
|
// A client with no list-changed handlers does not auto-listen, so the
|
||||||
|
// listen opened below is the session's only one.
|
||||||
|
c := NewClient(testImpl, nil)
|
||||||
|
c.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
|
||||||
|
return func(ctx context.Context, method string, req Request) (Result, error) {
|
||||||
|
if method == notificationSubscriptionsAck {
|
||||||
|
events <- subListenEvent{"ack", ""}
|
||||||
|
}
|
||||||
|
return next(ctx, method, req)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{ProtocolVersion: protocolVersion20260728})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client connect: %v", err)
|
||||||
|
}
|
||||||
|
defer cs.Close()
|
||||||
|
|
||||||
|
listenCtx, cancelListen := context.WithCancel(context.Background())
|
||||||
|
defer cancelListen()
|
||||||
|
if err := cs.subscriptionsListen(listenCtx, &SubscriptionsListenParams{
|
||||||
|
Notifications: &NotificationSubscriptions{ToolsListChanged: true, PromptsListChanged: true},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
waitSubListenEvent(t, events, "ack")
|
||||||
|
|
||||||
|
server.mu.Lock()
|
||||||
|
_, inTool := server.toolChangeSubscriptions[ss]
|
||||||
|
_, inPrompt := server.promptChangeSubscriptions[ss]
|
||||||
|
server.mu.Unlock()
|
||||||
|
if !inTool || !inPrompt {
|
||||||
|
t.Fatal("listen not registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelListen()
|
||||||
|
waitUntil(t, 5*time.Second, "registrations to be retired", func() bool {
|
||||||
|
server.mu.Lock()
|
||||||
|
defer server.mu.Unlock()
|
||||||
|
_, inTool := server.toolChangeSubscriptions[ss]
|
||||||
|
_, inPrompt := server.promptChangeSubscriptions[ss]
|
||||||
|
return !inTool && !inPrompt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitUntil polls cond until it reports true, failing the test after timeout.
|
||||||
|
func waitUntil(t *testing.T, timeout time.Duration, what string, cond func() bool) {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for !cond() {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatalf("timed out waiting for %s", what)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSubscriptionsListen_RespectsServerCapabilities verifies that during
|
// TestSubscriptionsListen_RespectsServerCapabilities verifies that during
|
||||||
// Connect the client only opens a SEP-2575 subscriptions/listen stream for the
|
// Connect the client only opens a SEP-2575 subscriptions/listen stream for the
|
||||||
// change notifications the server advertised during capability negotiation.
|
// change notifications the server advertised during capability negotiation.
|
||||||
|
|
|
||||||
19
mcp/mrtr.go
19
mcp/mrtr.go
|
|
@ -36,29 +36,20 @@ type multiRoundTripResponse interface {
|
||||||
hasContent() bool
|
hasContent() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMultiRoundTripResult(ss *ServerSession, logger *slog.Logger, res multiRoundTripResponse) error {
|
// validateMultiRoundTripResult rejects a result that carries both content and
|
||||||
|
// input requests. [annotateResultType] labels the result later, in
|
||||||
|
// [ServerSession.handle], so this function does not set resultType.
|
||||||
|
func validateMultiRoundTripResult(logger *slog.Logger, res multiRoundTripResponse) error {
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
hasInputRequests := res.inputRequests() != nil
|
if res.inputRequests() != nil && res.hasContent() {
|
||||||
|
|
||||||
if hasInputRequests && res.hasContent() {
|
|
||||||
logger.Warn("handler returned both content and inputRequests")
|
logger.Warn("handler returned both content and inputRequests")
|
||||||
return &jsonrpc.Error{
|
return &jsonrpc.Error{
|
||||||
Code: jsonrpc.CodeInternalError,
|
Code: jsonrpc.CodeInternalError,
|
||||||
Message: "server bug: result has both content and inputRequests",
|
Message: "server bug: result has both content and inputRequests",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientSupportsMultiRoundTrip(ss) {
|
|
||||||
// For older clients the resultType is left unset. Input requests will be handled
|
|
||||||
// by serverMultiRoundTripMiddleware client calls and handler reinvocation.
|
|
||||||
if hasInputRequests {
|
|
||||||
res.setResultType(resultTypeInputRequired)
|
|
||||||
} else {
|
|
||||||
res.setResultType(resultTypeComplete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
114
mcp/protocol.go
114
mcp/protocol.go
|
|
@ -19,7 +19,7 @@ type resultType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// resultTypeComplete indicates the result is final.
|
// resultTypeComplete indicates the result is final.
|
||||||
// This is the default when ResultType is empty.
|
// This is the default when resultType is empty.
|
||||||
resultTypeComplete resultType = "complete"
|
resultTypeComplete resultType = "complete"
|
||||||
|
|
||||||
// resultTypeInputRequired indicates the server needs additional client
|
// resultTypeInputRequired indicates the server needs additional client
|
||||||
|
|
@ -40,9 +40,20 @@ type completeResultResponse interface {
|
||||||
isCompleteResult()
|
isCompleteResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCompleteResultType(res Result) {
|
// annotateResultType sets the resultType that protocol 2026-07-28 requires on
|
||||||
if r, ok := res.(completeResultResponse); ok {
|
// every result.
|
||||||
|
func annotateResultType(res Result) {
|
||||||
|
switch r := res.(type) {
|
||||||
|
case completeResultResponse:
|
||||||
r.setResultType(resultTypeComplete)
|
r.setResultType(resultTypeComplete)
|
||||||
|
case multiRoundTripResponse:
|
||||||
|
// These results are complete or input_required, so label them by
|
||||||
|
// whether the handler asked for more client input.
|
||||||
|
if r.inputRequests() != nil {
|
||||||
|
r.setResultType(resultTypeInputRequired)
|
||||||
|
} else {
|
||||||
|
r.setResultType(resultTypeComplete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,9 +237,8 @@ type CallToolParams struct {
|
||||||
// marshaled to JSON.
|
// marshaled to JSON.
|
||||||
Arguments any `json:"arguments,omitempty"`
|
Arguments any `json:"arguments,omitempty"`
|
||||||
|
|
||||||
// InputResponses maps input request IDs to responses, provided when
|
// InputResponses maps input request IDs to responses. Set it when the
|
||||||
// retrying a call after receiving a result with ResultType
|
// client retries a call that asked for input. See [CallToolResult.NeedsInput].
|
||||||
// ResultTypeInputRequired.
|
|
||||||
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
||||||
// RequestState is the opaque state from the previous input-required result.
|
// RequestState is the opaque state from the previous input-required result.
|
||||||
// The client must echo this back when retrying.
|
// The client must echo this back when retrying.
|
||||||
|
|
@ -249,9 +259,8 @@ type CallToolParamsRaw struct {
|
||||||
// Arguments (see [AddTool]).
|
// Arguments (see [AddTool]).
|
||||||
Arguments json.RawMessage `json:"arguments,omitempty"`
|
Arguments json.RawMessage `json:"arguments,omitempty"`
|
||||||
|
|
||||||
// InputResponses maps input request IDs to responses, provided when
|
// InputResponses maps input request IDs to responses. Set it when the
|
||||||
// retrying a call after receiving a result with ResultType
|
// client retries a call that asked for input. See [CallToolResult.NeedsInput].
|
||||||
// ResultTypeInputRequired.
|
|
||||||
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
||||||
// RequestState is the opaque state from the previous input-required result.
|
// RequestState is the opaque state from the previous input-required result.
|
||||||
// The client must echo this back when retrying.
|
// The client must echo this back when retrying.
|
||||||
|
|
@ -280,6 +289,8 @@ type CallToolResult struct {
|
||||||
// result of the tool call. Per SEP-2106, it may marshal to any valid JSON
|
// result of the tool call. Per SEP-2106, it may marshal to any valid JSON
|
||||||
// value (object, array, or primitive) conforming to the tool's
|
// value (object, array, or primitive) conforming to the tool's
|
||||||
// [Tool.OutputSchema].
|
// [Tool.OutputSchema].
|
||||||
|
// Numbers received from the wire are represented as [json.Number] to preserve
|
||||||
|
// their exact values.
|
||||||
//
|
//
|
||||||
// When using a [ToolHandlerFor] with structured output, you should not
|
// When using a [ToolHandlerFor] with structured output, you should not
|
||||||
// populate this field. It will be automatically populated with the typed Out
|
// populate this field. It will be automatically populated with the typed Out
|
||||||
|
|
@ -305,9 +316,9 @@ type CallToolResult struct {
|
||||||
IsError bool `json:"isError,omitempty"`
|
IsError bool `json:"isError,omitempty"`
|
||||||
|
|
||||||
// InputRequests is a map of server-assigned IDs to input requests.
|
// InputRequests is a map of server-assigned IDs to input requests.
|
||||||
// Populated only when ResultType is ResultTypeInputRequired.
|
// It is set only when the result needs more client input.
|
||||||
// The client must fulfill these and echo the IDs back in InputResponses
|
// The client must fulfill these and echo the IDs back in InputResponses
|
||||||
// when retrying the call.
|
// when it retries the call.
|
||||||
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
||||||
|
|
||||||
// RequestState is an opaque string the client must echo back when
|
// RequestState is an opaque string the client must echo back when
|
||||||
|
|
@ -317,10 +328,10 @@ type CallToolResult struct {
|
||||||
// Unauthenticated servers must encrypt, sign and verify this value.
|
// Unauthenticated servers must encrypt, sign and verify this value.
|
||||||
RequestState string `json:"requestState,omitempty"`
|
RequestState string `json:"requestState,omitempty"`
|
||||||
|
|
||||||
// ResultType indicates whether this result is complete or requires further
|
// resultType records whether the call finished or needs more client input.
|
||||||
// client input. Empty or ResultTypeComplete means the call succeeded
|
// Empty or resultTypeComplete means it finished. resultTypeInputRequired
|
||||||
// normally. ResultTypeInputRequired means the client should fulfill the
|
// means the client must fulfill InputRequests and retry.
|
||||||
// InputRequests and retry the call.
|
// See [CallToolResult.NeedsInput].
|
||||||
resultType resultType
|
resultType resultType
|
||||||
// The error passed to setError, if any.
|
// The error passed to setError, if any.
|
||||||
// It is not marshaled, and therefore it is only visible on the server.
|
// It is not marshaled, and therefore it is only visible on the server.
|
||||||
|
|
@ -348,7 +359,8 @@ func (r *CallToolResult) GetError() error {
|
||||||
return r.err
|
return r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*CallToolResult) isResult() {}
|
func (*CallToolResult) isResult() {}
|
||||||
|
func (x *CallToolResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
func (r *CallToolResult) setResultType(rt resultType) { r.resultType = rt }
|
func (r *CallToolResult) setResultType(rt resultType) { r.resultType = rt }
|
||||||
func (r *CallToolResult) requestState() string { return r.RequestState }
|
func (r *CallToolResult) requestState() string { return r.RequestState }
|
||||||
|
|
@ -363,12 +375,18 @@ func (r *CallToolResult) hasContent() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedsInput reports whether this result requires further client input.
|
// NeedsInput reports whether this result requires further client input.
|
||||||
// This is true when the server returned ResultType "input_required".
|
// This is true when the server returned a resultType of "input_required".
|
||||||
// When NeedsInput returns true, check InputRequests for the set of
|
// When NeedsInput returns true, check InputRequests for the set of
|
||||||
// requests the server needs fulfilled before retrying the call.
|
// requests the server needs fulfilled before retrying the call.
|
||||||
// An empty InputRequests with NeedsInput true indicates load-shedding.
|
// An empty InputRequests with NeedsInput true indicates load-shedding.
|
||||||
func (r *CallToolResult) NeedsInput() bool { return r.resultType == resultTypeInputRequired }
|
func (r *CallToolResult) NeedsInput() bool { return r.resultType == resultTypeInputRequired }
|
||||||
|
|
||||||
|
// structuredcontentfloat64 is a compatibility parameter that restores the
|
||||||
|
// previous behavior of decoding numbers in [CallToolResult.StructuredContent]
|
||||||
|
// as float64 values. By default, numbers are decoded as [json.Number] to avoid
|
||||||
|
// losing precision. The option will be removed in the 1.11.0 version of the SDK.
|
||||||
|
var structuredcontentfloat64 = mcpgodebug.Value("structuredcontentfloat64")
|
||||||
|
|
||||||
func (x *CallToolResult) MarshalJSON() ([]byte, error) {
|
func (x *CallToolResult) MarshalJSON() ([]byte, error) {
|
||||||
type res CallToolResult // avoid recursion
|
type res CallToolResult // avoid recursion
|
||||||
type wire struct {
|
type wire struct {
|
||||||
|
|
@ -391,12 +409,22 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error {
|
||||||
type res CallToolResult // avoid recursion
|
type res CallToolResult // avoid recursion
|
||||||
var wire struct {
|
var wire struct {
|
||||||
res
|
res
|
||||||
Content []*wireContent `json:"content"`
|
Content []*wireContent `json:"content"`
|
||||||
ResultType resultType `json:"resultType"`
|
StructuredContent json.RawMessage `json:"structuredContent"`
|
||||||
|
ResultType resultType `json:"resultType"`
|
||||||
}
|
}
|
||||||
if err := internaljson.Unmarshal(data, &wire); err != nil {
|
if err := internaljson.Unmarshal(data, &wire); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(wire.StructuredContent) > 0 {
|
||||||
|
unmarshal := internaljson.UnmarshalUseNumber
|
||||||
|
if structuredcontentfloat64 == "1" {
|
||||||
|
unmarshal = internaljson.Unmarshal
|
||||||
|
}
|
||||||
|
if err := unmarshal(wire.StructuredContent, &wire.res.StructuredContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
|
if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -655,7 +683,8 @@ type CompleteResult struct {
|
||||||
Completion CompletionResultDetails `json:"completion"`
|
Completion CompletionResultDetails `json:"completion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*CompleteResult) isResult() {}
|
func (*CompleteResult) isResult() {}
|
||||||
|
func (x *CompleteResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
// CreateMessageParams holds parameters for a sampling/createMessage request.
|
// CreateMessageParams holds parameters for a sampling/createMessage request.
|
||||||
//
|
//
|
||||||
|
|
@ -839,6 +868,7 @@ type CreateMessageResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*CreateMessageResult) isResult() {}
|
func (*CreateMessageResult) isResult() {}
|
||||||
|
func (x *CreateMessageResult) isNil() bool { return x == nil }
|
||||||
func (*CreateMessageResult) isInputResponse() {}
|
func (*CreateMessageResult) isInputResponse() {}
|
||||||
func (r *CreateMessageResult) UnmarshalJSON(data []byte) error {
|
func (r *CreateMessageResult) UnmarshalJSON(data []byte) error {
|
||||||
type result CreateMessageResult // avoid recursion
|
type result CreateMessageResult // avoid recursion
|
||||||
|
|
@ -890,6 +920,7 @@ var createMessageWithToolsResultAllow = map[string]bool{
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*CreateMessageWithToolsResult) isResult() {}
|
func (*CreateMessageWithToolsResult) isResult() {}
|
||||||
|
func (x *CreateMessageWithToolsResult) isNil() bool { return x == nil }
|
||||||
func (*CreateMessageWithToolsResult) isInputResponse() {}
|
func (*CreateMessageWithToolsResult) isInputResponse() {}
|
||||||
|
|
||||||
// MarshalJSON marshals the result. When Content has a single element, it is
|
// MarshalJSON marshals the result. When Content has a single element, it is
|
||||||
|
|
@ -950,9 +981,8 @@ type GetPromptParams struct {
|
||||||
// The name of the prompt or prompt template.
|
// The name of the prompt or prompt template.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// InputResponses maps input request IDs to responses, provided when
|
// InputResponses maps input request IDs to responses. Set it when the
|
||||||
// retrying a call after receiving a result with ResultType
|
// client retries a call that asked for input. See [CallToolResult.NeedsInput].
|
||||||
// ResultTypeInputRequired.
|
|
||||||
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
||||||
// RequestState is the opaque state from the previous input-required result.
|
// RequestState is the opaque state from the previous input-required result.
|
||||||
RequestState string `json:"requestState,omitempty"`
|
RequestState string `json:"requestState,omitempty"`
|
||||||
|
|
@ -972,19 +1002,20 @@ type GetPromptResult struct {
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Messages []*PromptMessage `json:"messages"`
|
Messages []*PromptMessage `json:"messages"`
|
||||||
|
|
||||||
// InputRequests is populated when ResultType is ResultTypeInputRequired.
|
// InputRequests is set when the result needs more client input.
|
||||||
// See [CallToolResult.InputRequests].
|
// See [CallToolResult.InputRequests].
|
||||||
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
||||||
// RequestState is the opaque state for multi-round-trip retries.
|
// RequestState is the opaque state for multi-round-trip retries.
|
||||||
// See [CallToolResult.RequestState].
|
// See [CallToolResult.RequestState].
|
||||||
RequestState string `json:"requestState,omitempty"`
|
RequestState string `json:"requestState,omitempty"`
|
||||||
|
|
||||||
// ResultType indicates whether this result is complete or requires further
|
// resultType records whether the call finished or needs more client input.
|
||||||
// client input. See [CallToolResult.ResultType] for details.
|
// See [CallToolResult.NeedsInput].
|
||||||
resultType resultType
|
resultType resultType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*GetPromptResult) isResult() {}
|
func (*GetPromptResult) isResult() {}
|
||||||
|
func (x *GetPromptResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
func (r *GetPromptResult) setResultType(rt resultType) { r.resultType = rt }
|
func (r *GetPromptResult) setResultType(rt resultType) { r.resultType = rt }
|
||||||
func (r *GetPromptResult) requestState() string { return r.RequestState }
|
func (r *GetPromptResult) requestState() string { return r.RequestState }
|
||||||
|
|
@ -1094,7 +1125,8 @@ type InitializeResult struct {
|
||||||
ServerInfo *Implementation `json:"serverInfo"`
|
ServerInfo *Implementation `json:"serverInfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*InitializeResult) isResult() {}
|
func (*InitializeResult) isResult() {}
|
||||||
|
func (x *InitializeResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
type InitializedParams struct {
|
type InitializedParams struct {
|
||||||
// Meta is reserved by the protocol to allow clients and servers to attach
|
// Meta is reserved by the protocol to allow clients and servers to attach
|
||||||
|
|
@ -1137,7 +1169,8 @@ type DiscoverResult struct {
|
||||||
Instructions string `json:"instructions,omitempty"`
|
Instructions string `json:"instructions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*DiscoverResult) isResult() {}
|
func (*DiscoverResult) isResult() {}
|
||||||
|
func (x *DiscoverResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
func (x *ListPromptsParams) isParams() {}
|
func (x *ListPromptsParams) isParams() {}
|
||||||
func (x *ListPromptsParams) isNil() bool { return x == nil }
|
func (x *ListPromptsParams) isNil() bool { return x == nil }
|
||||||
|
|
@ -1207,6 +1240,7 @@ type ListPromptsResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListPromptsResult) isResult() {}
|
func (x *ListPromptsResult) isResult() {}
|
||||||
|
func (x *ListPromptsResult) isNil() bool { return x == nil }
|
||||||
func (x *ListPromptsResult) nextCursorPtr() *string { return &x.NextCursor }
|
func (x *ListPromptsResult) nextCursorPtr() *string { return &x.NextCursor }
|
||||||
|
|
||||||
type ListResourceTemplatesParams struct {
|
type ListResourceTemplatesParams struct {
|
||||||
|
|
@ -1238,6 +1272,7 @@ type ListResourceTemplatesResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListResourceTemplatesResult) isResult() {}
|
func (x *ListResourceTemplatesResult) isResult() {}
|
||||||
|
func (x *ListResourceTemplatesResult) isNil() bool { return x == nil }
|
||||||
func (x *ListResourceTemplatesResult) nextCursorPtr() *string { return &x.NextCursor }
|
func (x *ListResourceTemplatesResult) nextCursorPtr() *string { return &x.NextCursor }
|
||||||
|
|
||||||
type ListResourcesParams struct {
|
type ListResourcesParams struct {
|
||||||
|
|
@ -1269,6 +1304,7 @@ type ListResourcesResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListResourcesResult) isResult() {}
|
func (x *ListResourcesResult) isResult() {}
|
||||||
|
func (x *ListResourcesResult) isNil() bool { return x == nil }
|
||||||
func (x *ListResourcesResult) nextCursorPtr() *string { return &x.NextCursor }
|
func (x *ListResourcesResult) nextCursorPtr() *string { return &x.NextCursor }
|
||||||
|
|
||||||
// ListRootsParams holds parameters for a roots/list request.
|
// ListRootsParams holds parameters for a roots/list request.
|
||||||
|
|
@ -1305,6 +1341,7 @@ type ListRootsResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ListRootsResult) isResult() {}
|
func (*ListRootsResult) isResult() {}
|
||||||
|
func (x *ListRootsResult) isNil() bool { return x == nil }
|
||||||
func (*ListRootsResult) isInputResponse() {}
|
func (*ListRootsResult) isInputResponse() {}
|
||||||
|
|
||||||
type ListToolsParams struct {
|
type ListToolsParams struct {
|
||||||
|
|
@ -1336,6 +1373,7 @@ type ListToolsResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListToolsResult) isResult() {}
|
func (x *ListToolsResult) isResult() {}
|
||||||
|
func (x *ListToolsResult) isNil() bool { return x == nil }
|
||||||
func (x *ListToolsResult) nextCursorPtr() *string { return &x.NextCursor }
|
func (x *ListToolsResult) nextCursorPtr() *string { return &x.NextCursor }
|
||||||
|
|
||||||
// The severity of a log message.
|
// The severity of a log message.
|
||||||
|
|
@ -1575,9 +1613,8 @@ type ReadResourceParams struct {
|
||||||
// the server how to interpret it.
|
// the server how to interpret it.
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
|
|
||||||
// InputResponses maps input request IDs to responses, provided when
|
// InputResponses maps input request IDs to responses. Set it when the
|
||||||
// retrying a call after receiving a result with ResultType
|
// client retries a call that asked for input. See [CallToolResult.NeedsInput].
|
||||||
// ResultTypeInputRequired.
|
|
||||||
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
InputResponses InputResponseMap `json:"inputResponses,omitempty"`
|
||||||
// RequestState is the opaque state from the previous input-required result.
|
// RequestState is the opaque state from the previous input-required result.
|
||||||
RequestState string `json:"requestState,omitempty"`
|
RequestState string `json:"requestState,omitempty"`
|
||||||
|
|
@ -1596,19 +1633,20 @@ type ReadResourceResult struct {
|
||||||
Cacheable
|
Cacheable
|
||||||
Contents []*ResourceContents `json:"contents"`
|
Contents []*ResourceContents `json:"contents"`
|
||||||
|
|
||||||
// InputRequests is populated when ResultType is ResultTypeInputRequired.
|
// InputRequests is set when the result needs more client input.
|
||||||
// See [CallToolResult.InputRequests].
|
// See [CallToolResult.InputRequests].
|
||||||
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
InputRequests InputRequestMap `json:"inputRequests,omitempty"`
|
||||||
// RequestState is the opaque state for multi-round-trip retries.
|
// RequestState is the opaque state for multi-round-trip retries.
|
||||||
// See [CallToolResult.RequestState].
|
// See [CallToolResult.RequestState].
|
||||||
RequestState string `json:"requestState,omitempty"`
|
RequestState string `json:"requestState,omitempty"`
|
||||||
|
|
||||||
// ResultType indicates whether this result is complete or requires further
|
// resultType records whether the call finished or needs more client input.
|
||||||
// client input. See [CallToolResult.ResultType] for details.
|
// See [CallToolResult.NeedsInput].
|
||||||
resultType resultType
|
resultType resultType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ReadResourceResult) isResult() {}
|
func (*ReadResourceResult) isResult() {}
|
||||||
|
func (x *ReadResourceResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
func (r *ReadResourceResult) setResultType(rt resultType) { r.resultType = rt }
|
func (r *ReadResourceResult) setResultType(rt resultType) { r.resultType = rt }
|
||||||
func (r *ReadResourceResult) requestState() string { return r.RequestState }
|
func (r *ReadResourceResult) requestState() string { return r.RequestState }
|
||||||
|
|
@ -2114,7 +2152,8 @@ type SubscriptionsListenResult struct {
|
||||||
Meta `json:"_meta"`
|
Meta `json:"_meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*SubscriptionsListenResult) isResult() {}
|
func (*SubscriptionsListenResult) isResult() {}
|
||||||
|
func (x *SubscriptionsListenResult) isNil() bool { return x == nil }
|
||||||
|
|
||||||
// TODO(jba): add CompleteRequest and related types.
|
// TODO(jba): add CompleteRequest and related types.
|
||||||
|
|
||||||
|
|
@ -2191,6 +2230,7 @@ type ElicitResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ElicitResult) isResult() {}
|
func (*ElicitResult) isResult() {}
|
||||||
|
func (x *ElicitResult) isNil() bool { return x == nil }
|
||||||
func (*ElicitResult) isInputResponse() {}
|
func (*ElicitResult) isInputResponse() {}
|
||||||
|
|
||||||
// ElicitationCompleteParams is sent from the server to the client, informing it that an out-of-band elicitation interaction has completed.
|
// ElicitationCompleteParams is sent from the server to the client, informing it that an out-of-band elicitation interaction has completed.
|
||||||
|
|
|
||||||
|
|
@ -1285,7 +1285,7 @@ func TestContentUnmarshal(t *testing.T) {
|
||||||
Meta: Meta{"m": true},
|
Meta: Meta{"m": true},
|
||||||
Content: content,
|
Content: content,
|
||||||
IsError: true,
|
IsError: true,
|
||||||
StructuredContent: 3.0,
|
StructuredContent: json.Number("3"),
|
||||||
}
|
}
|
||||||
var gotf CallToolResult
|
var gotf CallToolResult
|
||||||
roundtrip(ctrf, &gotf)
|
roundtrip(ctrf, &gotf)
|
||||||
|
|
@ -1298,6 +1298,24 @@ func TestContentUnmarshal(t *testing.T) {
|
||||||
roundtrip(pm, &gotpm)
|
roundtrip(pm, &gotpm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCallToolResultStructuredContentFloat64Compatibility(t *testing.T) {
|
||||||
|
prev := structuredcontentfloat64
|
||||||
|
structuredcontentfloat64 = "1"
|
||||||
|
t.Cleanup(func() { structuredcontentfloat64 = prev })
|
||||||
|
|
||||||
|
var got CallToolResult
|
||||||
|
if err := json.Unmarshal([]byte(`{"content":[],"structuredContent":{"id":9007199254740993}}`), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
structured, ok := got.StructuredContent.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("StructuredContent type = %T, want map[string]any", got.StructuredContent)
|
||||||
|
}
|
||||||
|
if got := structured["id"]; got != float64(9007199254740992) {
|
||||||
|
t.Errorf("id = %v (%T), want rounded float64 value", got, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestToolAnnotations_MarshalJSON(t *testing.T) {
|
func TestToolAnnotations_MarshalJSON(t *testing.T) {
|
||||||
boolPtr := func(b bool) *bool { return &b }
|
boolPtr := func(b bool) *bool { return &b }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -910,7 +910,7 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm
|
||||||
}
|
}
|
||||||
res, err := prompt.handler(ctx, req)
|
res, err := prompt.handler(ctx, req)
|
||||||
if err == nil && res != nil {
|
if err == nil && res != nil {
|
||||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
if err := validateMultiRoundTripResult(s.opts.Logger, res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1012,10 +1012,10 @@ func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolR
|
||||||
}
|
}
|
||||||
res, err := st.handler(ctx, req)
|
res, err := st.handler(ctx, req)
|
||||||
if err == nil && res != nil {
|
if err == nil && res != nil {
|
||||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
if err := validateMultiRoundTripResult(s.opts.Logger, res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if res.Content == nil && res.resultType != resultTypeInputRequired {
|
if res.Content == nil && res.InputRequests == nil {
|
||||||
res2 := *res
|
res2 := *res
|
||||||
res2.Content = []Content{} // avoid "null"
|
res2.Content = []Content{} // avoid "null"
|
||||||
res = &res2
|
res = &res2
|
||||||
|
|
@ -1080,11 +1080,11 @@ func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*R
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return nil, fmt.Errorf("reading resource %s: read handler returned nil information", uri)
|
return nil, fmt.Errorf("reading resource %s: read handler returned nil information", uri)
|
||||||
}
|
}
|
||||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
if err := validateMultiRoundTripResult(s.opts.Logger, res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.resolveCacheable(ctx, req, &res.Cacheable)
|
s.resolveCacheable(ctx, req, &res.Cacheable)
|
||||||
if res.resultType == resultTypeInputRequired {
|
if res.InputRequests != nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
if res.Contents == nil {
|
if res.Contents == nil {
|
||||||
|
|
@ -1276,10 +1276,19 @@ func (s *Server) subscriptionsListen(ctx context.Context, req *SubscriptionsList
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
// Retire only what this listen registered above: a session can hold
|
||||||
|
// several listens at once, and deleting by session alone retires a
|
||||||
|
// registration belonging to a listen that is still open.
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
delete(s.toolChangeSubscriptions, req.Session)
|
if allowed.ToolsListChanged {
|
||||||
delete(s.promptChangeSubscriptions, req.Session)
|
delete(s.toolChangeSubscriptions, req.Session)
|
||||||
delete(s.resourceChangeSubscriptions, req.Session)
|
}
|
||||||
|
if allowed.PromptsListChanged {
|
||||||
|
delete(s.promptChangeSubscriptions, req.Session)
|
||||||
|
}
|
||||||
|
if allowed.ResourcesListChanged {
|
||||||
|
delete(s.resourceChangeSubscriptions, req.Session)
|
||||||
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1857,6 +1866,9 @@ func (s *Server) AddSendingMiddleware(middleware ...Middleware) {
|
||||||
//
|
//
|
||||||
// Receiving middleware is called when a request is received. It is useful for tasks
|
// Receiving middleware is called when a request is received. It is useful for tasks
|
||||||
// such as authentication, request logging and metrics.
|
// such as authentication, request logging and metrics.
|
||||||
|
//
|
||||||
|
// A received message need not carry params: use [HasParams] before inspecting
|
||||||
|
// [Request.GetParams].
|
||||||
func (s *Server) AddReceivingMiddleware(middleware ...Middleware) {
|
func (s *Server) AddReceivingMiddleware(middleware ...Middleware) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
@ -1986,10 +1998,6 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() {
|
|
||||||
ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method)
|
|
||||||
return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method)
|
|
||||||
}
|
|
||||||
if !initialized && validatedMeta.usesNewProtocol && validatedMeta.initializeParams != nil {
|
if !initialized && validatedMeta.usesNewProtocol && validatedMeta.initializeParams != nil {
|
||||||
ss.updateState(func(state *ServerSessionState) {
|
ss.updateState(func(state *ServerSessionState) {
|
||||||
state.InitializeParams = validatedMeta.initializeParams
|
state.InitializeParams = validatedMeta.initializeParams
|
||||||
|
|
@ -1997,6 +2005,16 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In legacy protocol versions, a client cannot send requests other than
|
||||||
|
// pings before the server has responded to the initialize request. A
|
||||||
|
// new-protocol request is exempt: a SEP-2575 session has no 'initialize'
|
||||||
|
// to wait for, and the request itself says which protocol it speaks.
|
||||||
|
if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() &&
|
||||||
|
req.Method != methodInitialize && req.Method != methodPing {
|
||||||
|
ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method)
|
||||||
|
return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method)
|
||||||
|
}
|
||||||
|
|
||||||
// modelcontextprotocol/go-sdk#26: handle calls asynchronously, and
|
// modelcontextprotocol/go-sdk#26: handle calls asynchronously, and
|
||||||
// notifications synchronously, except for 'initialize' which shouldn't be
|
// notifications synchronously, except for 'initialize' which shouldn't be
|
||||||
// asynchronous to other
|
// asynchronous to other
|
||||||
|
|
@ -2039,8 +2057,10 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if validatedMeta.usesNewProtocol {
|
// A middleware can return a typed nil value that satisfies the Result
|
||||||
setCompleteResultType(res)
|
// interface. Annotating that value panics.
|
||||||
|
if validatedMeta.usesNewProtocol && res != nil && !res.isNil() {
|
||||||
|
annotateResultType(res)
|
||||||
annotateServerInfo(res, ss.server.impl)
|
annotateServerInfo(res, ss.server.impl)
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
||||||
|
|
@ -677,7 +677,7 @@ func TestAddToolNonObjectOutputSchema(t *testing.T) {
|
||||||
name: "primitive number (map-based schema)",
|
name: "primitive number (map-based schema)",
|
||||||
outputSchema: map[string]any{"type": "number"},
|
outputSchema: map[string]any{"type": "number"},
|
||||||
content: 42.0,
|
content: 42.0,
|
||||||
want: 42.0,
|
want: json.Number("42"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "primitive string (RawMessage schema)",
|
name: "primitive string (RawMessage schema)",
|
||||||
|
|
@ -724,6 +724,52 @@ func TestAddToolNonObjectOutputSchema(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCallToolStructuredContentPreservesLargeInteger(t *testing.T) {
|
||||||
|
const want int64 = 9007199254740993
|
||||||
|
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
server.AddTool(&Tool{
|
||||||
|
Name: "large-integer",
|
||||||
|
InputSchema: &jsonschema.Schema{Type: "object"},
|
||||||
|
}, func(context.Context, *CallToolRequest) (*CallToolResult, error) {
|
||||||
|
return &CallToolResult{StructuredContent: map[string]any{"id": want}}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
clientTransport, serverTransport := NewInMemoryTransports()
|
||||||
|
serverSession, err := server.Connect(context.Background(), serverTransport, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer serverSession.Close()
|
||||||
|
|
||||||
|
client := NewClient(testImpl, nil)
|
||||||
|
clientSession, err := client.Connect(context.Background(), clientTransport, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer clientSession.Close()
|
||||||
|
|
||||||
|
result, err := clientSession.CallTool(context.Background(), &CallToolParams{Name: "large-integer"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
structured, ok := result.StructuredContent.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("StructuredContent type = %T, want map[string]any", result.StructuredContent)
|
||||||
|
}
|
||||||
|
number, ok := structured["id"].(json.Number)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("id type = %T, want json.Number", structured["id"])
|
||||||
|
}
|
||||||
|
got, err := number.Int64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("id = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestAddToolGenericNonObjectOutput verifies SEP-2106 for the generic
|
// TestAddToolGenericNonObjectOutput verifies SEP-2106 for the generic
|
||||||
// AddTool[In, Out] helper: Out may be a slice or primitive whose inferred
|
// AddTool[In, Out] helper: Out may be a slice or primitive whose inferred
|
||||||
// JSON Schema has a non-object root.
|
// JSON Schema has a non-object root.
|
||||||
|
|
@ -816,7 +862,7 @@ func TestAddToolGenericNonObjectOutput(t *testing.T) {
|
||||||
if res.IsError {
|
if res.IsError {
|
||||||
t.Fatalf("unexpected tool error: %v", res.Content)
|
t.Fatalf("unexpected tool error: %v", res.Content)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(float64(42), res.StructuredContent); diff != "" {
|
if diff := cmp.Diff(json.Number("42"), res.StructuredContent); diff != "" {
|
||||||
t.Errorf("structured content mismatch (-want +got):\n%s", diff)
|
t.Errorf("structured content mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1399,6 +1445,22 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newProtocolParams merges fields into the _meta that opts a request into the
|
||||||
|
// sessionless protocol (SEP-2575).
|
||||||
|
func newProtocolParams(fields map[string]any) map[string]any {
|
||||||
|
params := map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: protocolVersion20260728,
|
||||||
|
MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"},
|
||||||
|
MetaKeyClientCapabilities: map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, v := range fields {
|
||||||
|
params[k] = v
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) {
|
func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) {
|
||||||
server := NewServer(testImpl, &ServerOptions{
|
server := NewServer(testImpl, &ServerOptions{
|
||||||
CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) {
|
CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) {
|
||||||
|
|
@ -1424,19 +1486,19 @@ func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) {
|
||||||
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
newProtocolParams := func(fields map[string]any) map[string]any {
|
AddTool(server, &Tool{Name: "toolComplete"}, func(context.Context, *CallToolRequest, struct{}) (*CallToolResult, any, error) {
|
||||||
params := map[string]any{
|
return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil, nil
|
||||||
"_meta": map[string]any{
|
})
|
||||||
MetaKeyProtocolVersion: protocolVersion20260728,
|
server.AddPrompt(&Prompt{Name: "promptComplete"}, func(context.Context, *GetPromptRequest) (*GetPromptResult, error) {
|
||||||
MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"},
|
return &GetPromptResult{
|
||||||
MetaKeyClientCapabilities: map[string]any{},
|
Messages: []*PromptMessage{{Role: "assistant", Content: &TextContent{Text: "ok"}}},
|
||||||
},
|
}, nil
|
||||||
}
|
})
|
||||||
for k, v := range fields {
|
server.AddResource(&Resource{URI: "test://resource-complete", Name: "resourceComplete"}, func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) {
|
||||||
params[k] = v
|
return &ReadResourceResult{
|
||||||
}
|
Contents: []*ResourceContents{{URI: "test://resource-complete", Text: "ok"}},
|
||||||
return params
|
}, nil
|
||||||
}
|
})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -1507,40 +1569,243 @@ func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) {
|
||||||
params: newProtocolParams(map[string]any{"uri": "test://resource"}),
|
params: newProtocolParams(map[string]any{"uri": "test://resource"}),
|
||||||
want: resultTypeInputRequired,
|
want: resultTypeInputRequired,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tool complete",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "toolComplete", "arguments": map[string]any{}}),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompt complete",
|
||||||
|
method: methodGetPrompt,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "promptComplete"}),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource complete",
|
||||||
|
method: methodReadResource,
|
||||||
|
params: newProtocolParams(map[string]any{"uri": "test://resource-complete"}),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := resultTypeOf(t, server, nil, tc.method, tc.params); got != string(tc.want) {
|
||||||
|
t.Errorf("resultType = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerSessionHandle_SetsResultTypeWhenMiddlewareShortCircuits is a
|
||||||
|
// regression test for #1225. User middleware wraps outside
|
||||||
|
// serverMultiRoundTripMiddleware, so a middleware that returns a result
|
||||||
|
// without calling next bypasses both that shim and the dispatcher.
|
||||||
|
func TestServerSessionHandle_SetsResultTypeWhenMiddlewareShortCircuits(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
params map[string]any
|
||||||
|
result Result
|
||||||
|
want resultType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tools/call",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "tool", "arguments": map[string]any{}}),
|
||||||
|
result: &CallToolResult{Content: []Content{&TextContent{Text: "denied"}}},
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompts/get",
|
||||||
|
method: methodGetPrompt,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "prompt"}),
|
||||||
|
result: &GetPromptResult{
|
||||||
|
Messages: []*PromptMessage{{Role: "assistant", Content: &TextContent{Text: "denied"}}},
|
||||||
|
},
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resources/read",
|
||||||
|
method: methodReadResource,
|
||||||
|
params: newProtocolParams(map[string]any{"uri": "test://resource"}),
|
||||||
|
result: &ReadResourceResult{
|
||||||
|
Contents: []*ResourceContents{{URI: "test://resource", Text: "denied"}},
|
||||||
|
},
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// setResultType is unexported, so populating InputRequests is
|
||||||
|
// the only way a middleware can ask for input.
|
||||||
|
name: "tools/call input required",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "tool", "arguments": map[string]any{}}),
|
||||||
|
result: &CallToolResult{
|
||||||
|
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
||||||
|
},
|
||||||
|
want: resultTypeInputRequired,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Nothing is registered: if the middleware fell through to next,
|
||||||
|
// dispatch would fail rather than return a result.
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
|
||||||
|
return func(ctx context.Context, method string, req Request) (Result, error) {
|
||||||
|
if method == tc.method {
|
||||||
|
return tc.result, nil
|
||||||
|
}
|
||||||
|
return next(ctx, method, req)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if got := resultTypeOf(t, server, nil, tc.method, tc.params); got != string(tc.want) {
|
||||||
|
t.Errorf("resultType = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerSessionHandle_ResultTypeGate pins when a result carries resultType.
|
||||||
|
// annotateResultType is the only setter, and handle gates it on the request's
|
||||||
|
// _meta rather than on the version the session negotiated. So the last case
|
||||||
|
// below gets no resultType even though the session speaks 2026-07-28. Only a
|
||||||
|
// client that mixes protocol versions reaches that, so it is recorded rather
|
||||||
|
// than fixed.
|
||||||
|
func TestServerSessionHandle_ResultTypeGate(t *testing.T) {
|
||||||
|
version := func(v string) func(*ServerSessionState) {
|
||||||
|
return func(s *ServerSessionState) {
|
||||||
|
s.InitializeParams = &InitializeParams{ProtocolVersion: v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callParams := map[string]any{"name": "tool", "arguments": map[string]any{}}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
state func(*ServerSessionState)
|
||||||
|
params map[string]any
|
||||||
|
want resultType // "" means the field must be absent
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "legacy session, no _meta",
|
||||||
|
state: version(protocolVersion20251125),
|
||||||
|
params: callParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "legacy session, new-protocol _meta",
|
||||||
|
state: version(protocolVersion20251125),
|
||||||
|
params: newProtocolParams(callParams),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new-protocol session, no _meta",
|
||||||
|
state: version(protocolVersion20260728),
|
||||||
|
params: callParams,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
// Short-circuit so this measures handle's gate, not the
|
||||||
|
// dispatcher's.
|
||||||
|
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
|
||||||
|
return func(context.Context, string, Request) (Result, error) {
|
||||||
|
return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if got := resultTypeOf(t, server, tc.state, methodCallTool, tc.params); got != string(tc.want) {
|
||||||
|
t.Errorf("resultType = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerSessionHandle_NilResultFromMiddleware checks that a middleware
|
||||||
|
// returning no result does not panic. A typed nil satisfies Result, so the
|
||||||
|
// annotation helpers would otherwise dereference it.
|
||||||
|
func TestServerSessionHandle_NilResultFromMiddleware(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
result Result
|
||||||
|
}{
|
||||||
|
{"nil interface", nil},
|
||||||
|
{"typed nil", (*CallToolResult)(nil)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
|
||||||
|
return func(context.Context, string, Request) (Result, error) {
|
||||||
|
return tc.result, nil
|
||||||
|
}
|
||||||
|
})
|
||||||
ss := &ServerSession{server: server}
|
ss := &ServerSession{server: server}
|
||||||
id, err := jsonrpc.MakeID("test")
|
id, err := jsonrpc.MakeID("test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
result, err := ss.handle(context.Background(), &jsonrpc.Request{
|
res, err := ss.handle(context.Background(), &jsonrpc.Request{
|
||||||
ID: id,
|
ID: id,
|
||||||
Method: tc.method,
|
Method: methodCallTool,
|
||||||
Params: mustMarshal(tc.params),
|
Params: mustMarshal(newProtocolParams(map[string]any{"name": "tool", "arguments": map[string]any{}})),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatalf("handle: %v", err)
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(result)
|
data, err := json.Marshal(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var got struct {
|
if got := string(data); got != "null" {
|
||||||
ResultType string `json:"resultType"`
|
t.Errorf("result = %s, want null", got)
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got.ResultType != string(tc.want) {
|
|
||||||
t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, tc.want, data)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resultTypeOf handles one request on a transportless session and reports the
|
||||||
|
// resultType of the marshaled result, or "" if the field is absent.
|
||||||
|
func resultTypeOf(t *testing.T, server *Server, state func(*ServerSessionState), method string, params map[string]any) string {
|
||||||
|
t.Helper()
|
||||||
|
ss := &ServerSession{server: server}
|
||||||
|
if state != nil {
|
||||||
|
ss.updateState(state)
|
||||||
|
}
|
||||||
|
id, err := jsonrpc.MakeID("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := ss.handle(context.Background(), &jsonrpc.Request{
|
||||||
|
ID: id,
|
||||||
|
Method: method,
|
||||||
|
Params: mustMarshal(params),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var fields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &fields); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
raw, ok := fields["resultType"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var got string
|
||||||
|
if err := json.Unmarshal(raw, &got); err != nil {
|
||||||
|
t.Fatalf("unmarshaling resultType from %s: %v", data, err)
|
||||||
|
}
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
|
||||||
// TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that
|
// TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that
|
||||||
// the methods removed by SEP-2575 (`initialize`, `notifications/initialized`,
|
// the methods removed by SEP-2575 (`initialize`, `notifications/initialized`,
|
||||||
// `ping`) all return Method not found when the request opts into the new
|
// `ping`) all return Method not found when the request opts into the new
|
||||||
|
|
@ -1932,3 +2197,217 @@ func TestServerSupportedProtocolVersions_NewProtocol(t *testing.T) {
|
||||||
t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff)
|
t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServerHandle_LegacyCallBeforeInitialize asserts that a call carrying no
|
||||||
|
// new-protocol `_meta` is refused on a session that has not completed
|
||||||
|
// 'initialize', whichever branch of the method switch it lands in. The
|
||||||
|
// handshake itself is what ends that state, and the lifecycle spec exempts
|
||||||
|
// pings, so those two are served.
|
||||||
|
//
|
||||||
|
// The gate used to sit in the default branch, so 'logging/setLevel',
|
||||||
|
// 'resources/subscribe' and 'resources/unsubscribe', which share a case that
|
||||||
|
// only refuses new-protocol requests, were served before the handshake. For
|
||||||
|
// subscribe that registered a session that never handshook as a subscriber,
|
||||||
|
// so the subtest also asserts that the refusal leaves no subscriber and runs
|
||||||
|
// no handler.
|
||||||
|
func TestServerHandle_LegacyCallBeforeInitialize(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const uri = "test://resource"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
params string
|
||||||
|
served bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "initialize",
|
||||||
|
method: methodInitialize,
|
||||||
|
params: fmt.Sprintf(`{"protocolVersion":%q,"capabilities":{},"clientInfo":{"name":"c","version":"1"}}`, protocolVersion20251125),
|
||||||
|
served: true,
|
||||||
|
},
|
||||||
|
{name: "ping", method: methodPing, params: `{}`, served: true},
|
||||||
|
{name: "tools/list", method: methodListTools, params: `{}`},
|
||||||
|
{name: "logging/setLevel", method: methodSetLevel, params: `{"level":"info"}`},
|
||||||
|
{name: "resources/subscribe", method: methodSubscribe, params: fmt.Sprintf(`{"uri":%q}`, uri)},
|
||||||
|
{name: "resources/unsubscribe", method: methodUnsubscribe, params: fmt.Sprintf(`{"uri":%q}`, uri)},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var handlerRan bool
|
||||||
|
server := NewServer(testImpl, &ServerOptions{
|
||||||
|
SubscribeHandler: func(context.Context, *SubscribeRequest) error {
|
||||||
|
handlerRan = true
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
UnsubscribeHandler: func(context.Context, *UnsubscribeRequest) error {
|
||||||
|
handlerRan = true
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
_, st := NewInMemoryTransports()
|
||||||
|
ss, err := server.Connect(ctx, st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("server.Connect: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Close()
|
||||||
|
|
||||||
|
res, err := ss.handle(ctx, &jsonrpc.Request{
|
||||||
|
ID: jsonrpc2.Int64ID(1),
|
||||||
|
Method: tc.method,
|
||||||
|
Params: json.RawMessage(tc.params),
|
||||||
|
})
|
||||||
|
if tc.served {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handle(%q) before initialize returned %v, want it served", tc.method, err)
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
t.Fatalf("handle(%q) before initialize returned no result", tc.method)
|
||||||
|
}
|
||||||
|
if tc.method == methodInitialize && ss.InitializeParams() == nil {
|
||||||
|
t.Errorf("initialize was answered without recording InitializeParams")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("handle(%q) served the call before initialize, want a refusal", tc.method)
|
||||||
|
}
|
||||||
|
if want := fmt.Sprintf("method %q is invalid during session initialization", tc.method); err.Error() != want {
|
||||||
|
t.Errorf("handle(%q) error = %q, want %q", tc.method, err.Error(), want)
|
||||||
|
}
|
||||||
|
if handlerRan {
|
||||||
|
t.Errorf("handle(%q) ran the server's handler for a call it refused", tc.method)
|
||||||
|
}
|
||||||
|
server.mu.Lock()
|
||||||
|
_, subscribed := server.resourceSubscriptions[uri][ss]
|
||||||
|
server.mu.Unlock()
|
||||||
|
if subscribed {
|
||||||
|
t.Errorf("handle(%q) left the session subscribed to %q", tc.method, uri)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerHandle_NewProtocolCallWithoutInitialize asserts that the gate
|
||||||
|
// stays off the SEP-2575 path. A fresh session that never sends 'initialize'
|
||||||
|
// is served a new-protocol 'tools/list', and the methods the gate now covers
|
||||||
|
// are answered, when a new-protocol request names them, with the error the
|
||||||
|
// new protocol already gave them (the method does not exist there), and not
|
||||||
|
// with the initialization refusal.
|
||||||
|
func TestServerHandle_NewProtocolCallWithoutInitialize(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const uri = "test://resource"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
params map[string]any
|
||||||
|
served bool
|
||||||
|
}{
|
||||||
|
{name: "tools/list", method: methodListTools, params: newProtocolParams(nil), served: true},
|
||||||
|
{name: "logging/setLevel", method: methodSetLevel, params: newProtocolParams(map[string]any{"level": "info"})},
|
||||||
|
{name: "resources/subscribe", method: methodSubscribe, params: newProtocolParams(map[string]any{"uri": uri})},
|
||||||
|
{name: "resources/unsubscribe", method: methodUnsubscribe, params: newProtocolParams(map[string]any{"uri": uri})},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, &ServerOptions{
|
||||||
|
SubscribeHandler: func(context.Context, *SubscribeRequest) error { return nil },
|
||||||
|
UnsubscribeHandler: func(context.Context, *UnsubscribeRequest) error { return nil },
|
||||||
|
})
|
||||||
|
_, st := NewInMemoryTransports()
|
||||||
|
ss, err := server.Connect(ctx, st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("server.Connect: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Close()
|
||||||
|
|
||||||
|
params, err := json.Marshal(tc.params)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshalling params: %v", err)
|
||||||
|
}
|
||||||
|
res, err := ss.handle(ctx, &jsonrpc.Request{
|
||||||
|
ID: jsonrpc2.Int64ID(1),
|
||||||
|
Method: tc.method,
|
||||||
|
Params: params,
|
||||||
|
})
|
||||||
|
if tc.served {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handle(%q) on a new-protocol session returned %v, want it served", tc.method, err)
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
t.Fatalf("handle(%q) on a new-protocol session returned no result", tc.method)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var jerr *jsonrpc.Error
|
||||||
|
if !errors.As(err, &jerr) {
|
||||||
|
t.Fatalf("handle(%q) returned %v, want a *jsonrpc.Error", tc.method, err)
|
||||||
|
}
|
||||||
|
if jerr.Code != jsonrpc.CodeMethodNotFound {
|
||||||
|
t.Errorf("handle(%q) error code = %d, want %d (removed in the new protocol)", tc.method, jerr.Code, jsonrpc.CodeMethodNotFound)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "invalid during session initialization") {
|
||||||
|
t.Errorf("handle(%q) applied the initialization gate to a new-protocol request: %v", tc.method, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerUnknownProtocolVersion_NewProtocol verifies that a request whose
|
||||||
|
// `_meta.protocolVersion` names a version the SDK does not know is rejected
|
||||||
|
// with [CodeUnsupportedProtocolVersion], and not served as a legacy handshake
|
||||||
|
// because its version string happens to sort below 2026-07-28.
|
||||||
|
func TestServerUnknownProtocolVersion_NewProtocol(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Each case names a version that is not in [supportedProtocolVersions] and
|
||||||
|
// sorts below 2026-07-28.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
version string
|
||||||
|
extraMeta string // further `_meta` entries, each with a leading comma
|
||||||
|
}{
|
||||||
|
// The versioning spec uses 1900-01-01 to illustrate this error.
|
||||||
|
{"documented example", "1900-01-01", fmt.Sprintf(`,%q:{}`, MetaKeyClientCapabilities)},
|
||||||
|
{"unreleased revision", "2025-11-24", fmt.Sprintf(`,%q:{}`, MetaKeyClientCapabilities)},
|
||||||
|
{"not a date", "1.0", fmt.Sprintf(`,%q:{}`, MetaKeyClientCapabilities)},
|
||||||
|
// The rest of the `_meta` triple is defined by the revision the request
|
||||||
|
// names, so its absence must not mask the version itself.
|
||||||
|
{"no clientCapabilities", "1900-01-01", ""},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
_, st := NewInMemoryTransports()
|
||||||
|
ss, err := server.Connect(ctx, st, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("server.Connect: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Close()
|
||||||
|
|
||||||
|
params := fmt.Sprintf(`{"_meta":{%q:%q%s}}`, MetaKeyProtocolVersion, tc.version, tc.extraMeta)
|
||||||
|
_, err = ss.handle(ctx, &jsonrpc.Request{
|
||||||
|
ID: jsonrpc2.Int64ID(1),
|
||||||
|
Method: methodListTools,
|
||||||
|
Params: json.RawMessage(params),
|
||||||
|
})
|
||||||
|
var jerr *jsonrpc.Error
|
||||||
|
if !errors.As(err, &jerr) {
|
||||||
|
t.Fatalf("handle returned %v, want a *jsonrpc.Error", err)
|
||||||
|
}
|
||||||
|
if jerr.Code != CodeUnsupportedProtocolVersion {
|
||||||
|
t.Fatalf("error code = %d, want %d", jerr.Code, CodeUnsupportedProtocolVersion)
|
||||||
|
}
|
||||||
|
var data UnsupportedProtocolVersionData
|
||||||
|
if err := json.Unmarshal(jerr.Data, &data); err != nil {
|
||||||
|
t.Fatalf("unmarshal error data: %v", err)
|
||||||
|
}
|
||||||
|
if data.Requested != tc.version {
|
||||||
|
t.Errorf("UnsupportedProtocolVersionData.Requested = %q, want %q", data.Requested, tc.version)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(SupportedProtocolVersions(), data.Supported); diff != "" {
|
||||||
|
t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -553,13 +553,19 @@ type validatedMeta struct {
|
||||||
|
|
||||||
// validateRequestMeta inspects a JSON-RPC request to detect whether it follows
|
// validateRequestMeta inspects a JSON-RPC request to detect whether it follows
|
||||||
// the >= 2026-07-28 protocol via the `_meta` field.
|
// the >= 2026-07-28 protocol via the `_meta` field.
|
||||||
// If the request has no _meta, or no protocolVersion in _meta, it returns a non-nil
|
// If the request has no _meta, no protocolVersion in _meta, or a protocolVersion
|
||||||
// validatedMeta with usesNewProtocol set to false, and a nil error.
|
// naming a supported version that predates 2026-07-28, it returns a non-nil
|
||||||
// If the request has a protocolVersion in _meta it validates the presence of
|
// validatedMeta with usesNewProtocol set to false, and a nil error: such a request
|
||||||
// clientCapabilities in _meta. If it is missing or invalid, it returns nil and
|
// belongs to the initialize handshake.
|
||||||
// a non-nil error. clientInfo is optional; if present but invalid, an error is
|
// If the protocolVersion names a version that is not supported at all, it returns
|
||||||
// returned. Otherwise, it returns usesNewProtocol set to true and the populated
|
// usesNewProtocol set to true with initializeParams carrying only that version, so
|
||||||
// initializeParams.
|
// that the caller answers with [CodeUnsupportedProtocolVersion]. The rest of the
|
||||||
|
// `_meta` triple is not validated in that case, since the revision defining it is
|
||||||
|
// unknown.
|
||||||
|
// Otherwise, it validates the presence of clientCapabilities in _meta. If it is
|
||||||
|
// missing or invalid, it returns nil and a non-nil error. clientInfo is optional;
|
||||||
|
// if present but invalid, an error is returned. Otherwise, it returns
|
||||||
|
// usesNewProtocol set to true and the populated initializeParams.
|
||||||
// Notifications always report usesNewProtocol false and never error.
|
// Notifications always report usesNewProtocol false and never error.
|
||||||
func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
||||||
// SEP-2575 defines the `_meta` triple for calls only: NotificationParams
|
// SEP-2575 defines the `_meta` triple for calls only: NotificationParams
|
||||||
|
|
@ -574,9 +580,26 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
||||||
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||||
}
|
}
|
||||||
protocolVersion, ok := meta[MetaKeyProtocolVersion].(string)
|
protocolVersion, ok := meta[MetaKeyProtocolVersion].(string)
|
||||||
if !ok || protocolVersion < protocolVersion20260728 {
|
// An absent or empty value declares no version at all, which is also how the
|
||||||
|
// streamable handler reads `_meta.protocolVersion`.
|
||||||
|
if !ok || protocolVersion == "" {
|
||||||
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||||
}
|
}
|
||||||
|
// Classify by membership rather than by ordering: a version the SDK does not
|
||||||
|
// know sorts arbitrarily against 2026-07-28, and one that sorts below it would
|
||||||
|
// be served as a legacy handshake instead of reaching the
|
||||||
|
// CodeUnsupportedProtocolVersion check in ServerSession.handle. The
|
||||||
|
// streamable handler already applies the same membership rule to the
|
||||||
|
// Mcp-Protocol-Version header.
|
||||||
|
supported := slices.Contains(supportedProtocolVersions, protocolVersion)
|
||||||
|
if supported && protocolVersion < protocolVersion20260728 {
|
||||||
|
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||||
|
}
|
||||||
|
if !supported {
|
||||||
|
return &validatedMeta{usesNewProtocol: true, initializeParams: &InitializeParams{
|
||||||
|
ProtocolVersion: protocolVersion,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
var clientInfo *Implementation
|
var clientInfo *Implementation
|
||||||
if _, present := meta[MetaKeyClientInfo]; present {
|
if _, present := meta[MetaKeyClientInfo]; present {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
@ -665,6 +688,30 @@ func (r *ServerRequest[P]) GetParams() Params { return r.Params }
|
||||||
func (r *ClientRequest[P]) GetExtra() *RequestExtra { return nil }
|
func (r *ClientRequest[P]) GetExtra() *RequestExtra { return nil }
|
||||||
func (r *ServerRequest[P]) GetExtra() *RequestExtra { return r.Extra }
|
func (r *ServerRequest[P]) GetExtra() *RequestExtra { return r.Extra }
|
||||||
|
|
||||||
|
// HasParams reports whether req carries params.
|
||||||
|
//
|
||||||
|
// The "params" member of a message may be omitted for several methods, among
|
||||||
|
// them the list methods, ping and notifications/initialized. For such a request
|
||||||
|
// [Request.GetParams] returns a non-nil [Params] holding a nil pointer, so
|
||||||
|
// comparing its result against nil reports that params are present, while
|
||||||
|
// calling a method on that result panics. Middleware that inspects params must
|
||||||
|
// ask this first:
|
||||||
|
//
|
||||||
|
// if mcp.HasParams(req) {
|
||||||
|
// log.Println(method, req.GetParams().GetMeta())
|
||||||
|
// }
|
||||||
|
func HasParams(req Request) bool {
|
||||||
|
params := req.GetParams()
|
||||||
|
if params == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// isNil cannot answer this for every value: a params type embedding
|
||||||
|
// ParamsBase promotes isNil through a field selector, which dereferences
|
||||||
|
// the nil outer pointer before the method body runs.
|
||||||
|
v := reflect.ValueOf(params)
|
||||||
|
return v.Kind() != reflect.Pointer || !v.IsNil()
|
||||||
|
}
|
||||||
|
|
||||||
// ProtocolVersion returns the protocol version negotiated for this request.
|
// ProtocolVersion returns the protocol version negotiated for this request.
|
||||||
//
|
//
|
||||||
// For requests following the >= 2026-07-28 protocol, the value is read from
|
// For requests following the >= 2026-07-28 protocol, the value is read from
|
||||||
|
|
@ -726,8 +773,7 @@ func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities {
|
||||||
// getRequestMeta returns the raw `_meta` map from the request's params, or
|
// getRequestMeta returns the raw `_meta` map from the request's params, or
|
||||||
// nil if the params are absent.
|
// nil if the params are absent.
|
||||||
func getRequestMeta[P Params](r *ServerRequest[P]) map[string]any {
|
func getRequestMeta[P Params](r *ServerRequest[P]) map[string]any {
|
||||||
// In practice P is a pointer type implementing Params.
|
if !HasParams(r) {
|
||||||
if any(r.Params) == nil || r.Params.isNil() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return r.Params.GetMeta()
|
return r.Params.GetMeta()
|
||||||
|
|
@ -809,6 +855,9 @@ type Result interface {
|
||||||
// isResult discourages implementation of Result outside of this package.
|
// isResult discourages implementation of Result outside of this package.
|
||||||
isResult()
|
isResult()
|
||||||
|
|
||||||
|
// isNil returns true if the underlying value is nil.
|
||||||
|
isNil() bool
|
||||||
|
|
||||||
// GetMeta returns metadata from a value.
|
// GetMeta returns metadata from a value.
|
||||||
GetMeta() map[string]any
|
GetMeta() map[string]any
|
||||||
// SetMeta sets the metadata on a value.
|
// SetMeta sets the metadata on a value.
|
||||||
|
|
@ -827,13 +876,15 @@ type ResultBase struct {
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ResultBase) isResult() {}
|
func (*ResultBase) isResult() {}
|
||||||
|
func (x *ResultBase) isNil() bool { return x == nil }
|
||||||
|
|
||||||
// emptyResult is returned by methods that have no result, like ping.
|
// emptyResult is returned by methods that have no result, like ping.
|
||||||
// Those methods cannot return nil, because jsonrpc2 cannot handle nils.
|
// Those methods cannot return nil, because jsonrpc2 cannot handle nils.
|
||||||
type emptyResult struct{}
|
type emptyResult struct{}
|
||||||
|
|
||||||
func (*emptyResult) isResult() {}
|
func (*emptyResult) isResult() {}
|
||||||
|
func (x *emptyResult) isNil() bool { return x == nil }
|
||||||
func (*emptyResult) GetMeta() map[string]any { panic("should never be called") }
|
func (*emptyResult) GetMeta() map[string]any { panic("should never be called") }
|
||||||
func (*emptyResult) SetMeta(map[string]any) { panic("should never be called") }
|
func (*emptyResult) SetMeta(map[string]any) { panic("should never be called") }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,57 @@ func TestValidateRequestMeta(t *testing.T) {
|
||||||
params: json.RawMessage(`{"_meta": "not an object", "name": "x"}`),
|
params: json.RawMessage(`{"_meta": "not an object", "name": "x"}`),
|
||||||
wantUsesNew: false,
|
wantUsesNew: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "supported pre-2026-07-28 version: old protocol",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: protocolVersion20251125,
|
||||||
|
},
|
||||||
|
"name": "x",
|
||||||
|
},
|
||||||
|
wantUsesNew: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty protocolVersion declares nothing: old protocol",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: "",
|
||||||
|
},
|
||||||
|
"name": "x",
|
||||||
|
},
|
||||||
|
wantUsesNew: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// An unsupported version sorting below 2026-07-28 is still a
|
||||||
|
// new-protocol request, so that the caller can answer it with
|
||||||
|
// CodeUnsupportedProtocolVersion.
|
||||||
|
name: "unsupported version: new protocol",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: "1900-01-01",
|
||||||
|
MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"},
|
||||||
|
MetaKeyClientCapabilities: map[string]any{},
|
||||||
|
},
|
||||||
|
"name": "x",
|
||||||
|
},
|
||||||
|
wantUsesNew: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The triple is defined by a revision the SDK does not know, so an
|
||||||
|
// unsupported version is reported without validating the rest of it.
|
||||||
|
name: "unsupported version missing clientCapabilities: new protocol",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: "1900-01-01",
|
||||||
|
},
|
||||||
|
"name": "x",
|
||||||
|
},
|
||||||
|
wantUsesNew: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "new protocol with logLevel",
|
name: "new protocol with logLevel",
|
||||||
method: methodCallTool,
|
method: methodCallTool,
|
||||||
|
|
@ -273,6 +324,62 @@ func TestServerRequest_PerRequestAccessors_Empty(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasParams(t *testing.T) {
|
||||||
|
// {"jsonrpc":"2.0","id":1,"method":"tools/list"} is a complete request:
|
||||||
|
// serverMethodInfos marks these methods missingParamsOK. Build what
|
||||||
|
// handleReceive hands the middleware chain for one.
|
||||||
|
for _, method := range []string{
|
||||||
|
methodListTools,
|
||||||
|
methodListPrompts,
|
||||||
|
methodListResources,
|
||||||
|
notificationInitialized,
|
||||||
|
} {
|
||||||
|
t.Run(method, func(t *testing.T) {
|
||||||
|
info := serverMethodInfos[method]
|
||||||
|
params, err := info.unmarshalParams(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req := info.newRequest(&ServerSession{}, params, nil)
|
||||||
|
if req.GetParams() == nil {
|
||||||
|
t.Fatal("GetParams returned a nil interface, so this test no longer covers the typed-nil case")
|
||||||
|
}
|
||||||
|
if HasParams(req) {
|
||||||
|
t.Error("HasParams = true for a request whose params member was omitted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("params present", func(t *testing.T) {
|
||||||
|
info := serverMethodInfos[methodListTools]
|
||||||
|
params, err := info.unmarshalParams(json.RawMessage(`{"cursor":"c"}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req := info.newRequest(&ServerSession{}, params, nil)
|
||||||
|
if !HasParams(req) {
|
||||||
|
t.Error("HasParams = false for a request carrying params")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerRequest_PerRequestAccessors_MissingParams(t *testing.T) {
|
||||||
|
// Custom methods also allow the params member to be omitted, and their
|
||||||
|
// params types embed ParamsBase, whose promoted isNil dereferences the
|
||||||
|
// typed-nil outer pointer.
|
||||||
|
type customParams struct{ ParamsBase }
|
||||||
|
req := &ServerRequest[*customParams]{}
|
||||||
|
if got := req.ProtocolVersion(); got != "" {
|
||||||
|
t.Errorf("ProtocolVersion = %q, want empty", got)
|
||||||
|
}
|
||||||
|
if got := req.ClientInfo(); got != nil {
|
||||||
|
t.Errorf("ClientInfo = %+v, want nil", got)
|
||||||
|
}
|
||||||
|
if got := req.ClientCapabilities(); got != nil {
|
||||||
|
t.Errorf("ClientCapabilities = %+v, want nil", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImplementationDescriptionJSON(t *testing.T) {
|
func TestImplementationDescriptionJSON(t *testing.T) {
|
||||||
impl := &Implementation{
|
impl := &Implementation{
|
||||||
Name: "greeter",
|
Name: "greeter",
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue