Skip to content

Commit fecbc92

Browse files
committed
mcp: allow requests before notifications/initialized
Upon a closer reading of the spec, it is OK for the server to response to client requests before it has received notifications/initialized (as long as it has received initialize). This effectively rolls back the fix from #225. Some hooks are left for enforcing strictness around server->client requests prior to initialized. These will be revisited in subsequent CLs. For #395
1 parent 9d34aff commit fecbc92

File tree

4 files changed

+53
-6
lines changed

4 files changed

+53
-6
lines changed

mcp/mcp_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
14151415
RequestedSchema: &jsonschema.Schema{Type: "object"},
14161416
})
14171417
if err != nil {
1418-
t.Errorf("elicitation should work when capability is declared, got error: %v", err)
1418+
t.Fatalf("elicitation should work when capability is declared, got error: %v", err)
14191419
}
14201420
if result.Action != "cancel" {
14211421
t.Errorf("got action %q, want %q", result.Action, "cancel")

mcp/server.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,33 @@ func (ss *ServerSession) updateState(mut func(*ServerSessionState)) {
844844
}
845845
}
846846

847+
// hasInitialized reports whether the server has received the initialized
848+
// notification.
849+
//
850+
// TODO(findleyr): use this to prevent change notifications.
851+
func (ss *ServerSession) hasInitialized() bool {
852+
ss.mu.Lock()
853+
defer ss.mu.Unlock()
854+
return ss.state.InitializedParams != nil
855+
}
856+
857+
// checkInitialized returns a formatted error if the server has not yet
858+
// received the initialized notification.
859+
func (ss *ServerSession) checkInitialized(method string) error {
860+
if !ss.hasInitialized() {
861+
// TODO(rfindley): enable this check.
862+
// Right now is is flaky, because server tests don't await the initialized notification.
863+
// Perhaps requests should simply block until they have received the initialized notification
864+
865+
// if strings.HasPrefix(method, "notifications/") {
866+
// return fmt.Errorf("must not send %q before %q is received", method, notificationInitialized)
867+
// } else {
868+
// return fmt.Errorf("cannot call %q before %q is received", method, notificationInitialized)
869+
// }
870+
}
871+
return nil
872+
}
873+
847874
func (ss *ServerSession) ID() string {
848875
if c, ok := ss.mcpConn.(hasSessionID); ok {
849876
return c.SessionID()
@@ -859,11 +886,17 @@ func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error {
859886

860887
// ListRoots lists the client roots.
861888
func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams) (*ListRootsResult, error) {
889+
if err := ss.checkInitialized(methodListRoots); err != nil {
890+
return nil, err
891+
}
862892
return handleSend[*ListRootsResult](ctx, methodListRoots, newServerRequest(ss, orZero[Params](params)))
863893
}
864894

865895
// CreateMessage sends a sampling request to the client.
866896
func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessageParams) (*CreateMessageResult, error) {
897+
if err := ss.checkInitialized(methodCreateMessage); err != nil {
898+
return nil, err
899+
}
867900
if params == nil {
868901
params = &CreateMessageParams{Messages: []*SamplingMessage{}}
869902
}
@@ -877,6 +910,9 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag
877910

878911
// Elicit sends an elicitation request to the client asking for user input.
879912
func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*ElicitResult, error) {
913+
if err := ss.checkInitialized(methodElicit); err != nil {
914+
return nil, err
915+
}
880916
return handleSend[*ElicitResult](ctx, methodElicit, newServerRequest(ss, orZero[Params](params)))
881917
}
882918

@@ -978,7 +1014,7 @@ func (ss *ServerSession) getConn() *jsonrpc2.Connection { return ss.conn }
9781014
// handle invokes the method described by the given JSON RPC request.
9791015
func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, error) {
9801016
ss.mu.Lock()
981-
initialized := ss.state.InitializedParams != nil
1017+
initialized := ss.state.InitializeParams != nil
9821018
ss.mu.Unlock()
9831019

9841020
// From the spec:

mcp/shared.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,12 @@ func notifySessions[S Session, P Params](sessions []S, method string, params P)
346346
if sessions == nil {
347347
return
348348
}
349-
// TODO: make this timeout configurable, or call Notify asynchronously.
349+
// TODO: make this timeout configurable, or call handleNotify asynchronously.
350350
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
351351
defer cancel()
352+
353+
// TODO: there's a potential spec violation here, when the feature list
354+
// changes before the session (client or server) is initialized.
352355
for _, s := range sessions {
353356
req := newRequest(s, params)
354357
if err := handleNotify(ctx, method, req); err != nil {

mcp/testdata/conformance/server/lifecycle.txtar

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ See also modelcontextprotocol/go-sdk#225.
55

66
-- client --
77
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
8+
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
89
{
910
"jsonrpc": "2.0",
1011
"id": 1,
@@ -21,6 +22,14 @@ See also modelcontextprotocol/go-sdk#225.
2122
{ "jsonrpc": "2.0", "id": 3, "method": "tools/list" }
2223

2324
-- server --
25+
{
26+
"jsonrpc": "2.0",
27+
"id": 2,
28+
"error": {
29+
"code": 0,
30+
"message": "method \"tools/list\" is invalid during session initialization"
31+
}
32+
}
2433
{
2534
"jsonrpc": "2.0",
2635
"id": 1,
@@ -43,9 +52,8 @@ See also modelcontextprotocol/go-sdk#225.
4352
{
4453
"jsonrpc": "2.0",
4554
"id": 2,
46-
"error": {
47-
"code": 0,
48-
"message": "method \"tools/list\" is invalid during session initialization"
55+
"result": {
56+
"tools": []
4957
}
5058
}
5159
{

0 commit comments

Comments
 (0)