From 80b8ea6e843c79f27c5f7e3693438732b69c5df6 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 2 Jun 2026 12:25:36 +0200 Subject: [PATCH 1/2] Refine Go SDK pre-GA API surfaces --- docs/auth/byok.md | 4 +- docs/features/mcp.md | 2 +- go/README.md | 11 +- go/client_test.go | 4 +- .../e2e/commands_and_elicitation_e2e_test.go | 60 ++++------ go/internal/e2e/mcp_and_agents_e2e_test.go | 4 +- go/internal/e2e/mcp_server_helpers_test.go | 2 +- .../e2e/pre_mcp_tool_call_hook_e2e_test.go | 3 +- .../e2e/rpc_tasks_and_handlers_e2e_test.go | 4 +- go/rpc/zrpc.go | 42 +++---- go/session.go | 113 ++++++++++-------- go/session_test.go | 59 ++++----- go/types.go | 60 ++++++---- go/types_test.go | 4 +- scripts/codegen/go.ts | 22 +++- 15 files changed, 216 insertions(+), 178 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 504602fd3..6f2d20633 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -409,7 +409,7 @@ func main() { Name: "My Custom Model", Capabilities: copilot.ModelCapabilities{ Supports: copilot.ModelSupports{Vision: false, ReasoningEffort: false}, - Limits: copilot.ModelLimits{MaxContextWindowTokens: 128000}, + Limits: copilot.ModelLimits{MaxContextWindowTokens: copilot.Int(128000)}, }, }, }, nil @@ -478,7 +478,7 @@ When using BYOK, be aware of these limitations: ### Identity limitations -BYOK authentication uses **static credentials only**. +BYOK authentication uses **static credentials only**. You must use an API key or static bearer token that you manage yourself. diff --git a/docs/features/mcp.md b/docs/features/mcp.md index e974532b0..6f715bd2e 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -120,7 +120,7 @@ func main() { "my-local-server": copilot.MCPStdioServerConfig{ Command: "node", Args: []string{"./mcp-server.js"}, - Tools: &[]string{"*"}, + Tools: []string{"*"}, }, }, }) diff --git a/go/README.md b/go/README.md index 92e7cad0f..0ceaabb73 100644 --- a/go/README.md +++ b/go/README.md @@ -810,11 +810,10 @@ name, ok, err := ui.Input(ctx, "Enter the release name", &copilot.UIInputOptions }) // Full custom elicitation with a schema -result, err := ui.Elicitation(ctx, "Configure deployment", rpc.RequestedSchema{ - Type: rpc.RequestedSchemaTypeObject, - Properties: map[string]rpc.Property{ - "target": {Type: rpc.PropertyTypeString, Enum: []string{"staging", "production"}}, - "force": {Type: rpc.PropertyTypeBoolean}, +result, err := ui.Elicitation(ctx, "Configure deployment", copilot.ElicitationSchema{ + Properties: map[string]any{ + "target": map[string]any{"type": "string", "enum": []string{"staging", "production"}}, + "force": map[string]any{"type": "boolean"}, }, Required: []string{"target"}, }) @@ -839,7 +838,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ // Return the user's response return copilot.ElicitationResult{ - Action: "accept", + Action: copilot.ElicitationActionAccept, Content: map[string]any{"confirmed": true}, }, nil }, diff --git a/go/client_test.go b/go/client_test.go index 6236a95ab..d5ba47da8 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -867,7 +867,7 @@ func TestListModelsWithCustomHandler(t *testing.T) { Name: "My Custom Model", Capabilities: ModelCapabilities{ Supports: ModelSupports{Vision: false, ReasoningEffort: false}, - Limits: ModelLimits{MaxContextWindowTokens: 128000}, + Limits: ModelLimits{MaxContextWindowTokens: Int(128000)}, }, }, } @@ -899,7 +899,7 @@ func TestListModelsHandlerCachesResults(t *testing.T) { Name: "Cached Model", Capabilities: ModelCapabilities{ Supports: ModelSupports{Vision: false, ReasoningEffort: false}, - Limits: ModelLimits{MaxContextWindowTokens: 128000}, + Limits: ModelLimits{MaxContextWindowTokens: Int(128000)}, }, }, } diff --git a/go/internal/e2e/commands_and_elicitation_e2e_test.go b/go/internal/e2e/commands_and_elicitation_e2e_test.go index 45d54dde9..8d2d40f2f 100644 --- a/go/internal/e2e/commands_and_elicitation_e2e_test.go +++ b/go/internal/e2e/commands_and_elicitation_e2e_test.go @@ -437,7 +437,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnElicitationRequest: func(ctx copilot.ElicitationContext) (copilot.ElicitationResult, error) { - return copilot.ElicitationResult{Action: "accept", Content: map[string]any{}}, nil + return copilot.ElicitationResult{Action: copilot.ElicitationActionAccept, Content: map[string]any{}}, nil }, }) if err != nil { @@ -481,7 +481,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { t.Errorf("Expected RequestedSchema to contain 'confirmed' property") } return copilot.ElicitationResult{ - Action: "accept", + Action: copilot.ElicitationActionAccept, Content: map[string]any{"confirmed": true}, }, nil }, @@ -505,7 +505,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnElicitationRequest: func(ec copilot.ElicitationContext) (copilot.ElicitationResult, error) { - return copilot.ElicitationResult{Action: "decline"}, nil + return copilot.ElicitationResult{Action: copilot.ElicitationActionDecline}, nil }, }) if err != nil { @@ -534,7 +534,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { t.Errorf("Expected RequestedSchema to contain 'selection' property") } return copilot.ElicitationResult{ - Action: "accept", + Action: copilot.ElicitationActionAccept, Content: map[string]any{"selection": "beta"}, }, nil }, @@ -568,7 +568,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { t.Errorf("Expected RequestedSchema to contain 'value' property") } return copilot.ElicitationResult{ - Action: "accept", + Action: copilot.ElicitationActionAccept, Content: map[string]any{"value": "typed value"}, }, nil }, @@ -601,9 +601,9 @@ func TestUIElicitationCallbackE2E(t *testing.T) { ctx.ConfigureForTest(t) responses := []copilot.ElicitationResult{ - {Action: "accept", Content: map[string]any{"name": "Mona"}}, - {Action: "decline"}, - {Action: "cancel"}, + {Action: copilot.ElicitationActionAccept, Content: map[string]any{"name": "Mona"}}, + {Action: copilot.ElicitationActionDecline}, + {Action: copilot.ElicitationActionCancel}, } var idx int @@ -625,9 +625,8 @@ func TestUIElicitationCallbackE2E(t *testing.T) { t.Fatalf("CreateSession failed: %v", err) } - schema := rpc.UIElicitationSchema{ - Type: rpc.UIElicitationSchemaTypeObject, - Properties: map[string]rpc.UIElicitationSchemaProperty{ + schema := copilot.ElicitationSchema{ + Properties: map[string]any{ "name": &rpc.UIElicitationSchemaPropertyString{}, }, Required: []string{"name"}, @@ -637,10 +636,10 @@ func TestUIElicitationCallbackE2E(t *testing.T) { if err != nil { t.Fatalf("Elicitation accept call failed: %v", err) } - if accept.Action != "accept" { + if accept.Action != copilot.ElicitationActionAccept { t.Errorf("Expected accept.Action='accept', got %q", accept.Action) } - if accept.Content == nil || fmt.Sprintf("%v", accept.Content["name"]) != "Mona" { + if accept.Content == nil || accept.Content["name"] != "Mona" { t.Errorf("Expected accept.Content[name]='Mona', got %v", accept.Content) } @@ -648,7 +647,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { if err != nil { t.Fatalf("Elicitation decline call failed: %v", err) } - if decline.Action != "decline" { + if decline.Action != copilot.ElicitationActionDecline { t.Errorf("Expected decline.Action='decline', got %q", decline.Action) } @@ -656,7 +655,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { if err != nil { t.Fatalf("Elicitation cancel call failed: %v", err) } - if cancel.Action != "cancel" { + if cancel.Action != copilot.ElicitationActionCancel { t.Errorf("Expected cancel.Action='cancel', got %q", cancel.Action) } }) @@ -681,7 +680,7 @@ func TestUIElicitationCallbackE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnElicitationRequest: func(ec copilot.ElicitationContext) (copilot.ElicitationResult, error) { - return copilot.ElicitationResult{Action: "accept", Content: map[string]any{}}, nil + return copilot.ElicitationResult{Action: copilot.ElicitationActionAccept, Content: map[string]any{}}, nil }, }) if err != nil { @@ -694,29 +693,14 @@ func TestUIElicitationCallbackE2E(t *testing.T) { }) } -// schemaHasProperty reports whether the elicitation schema map has a top-level -// property with the given name. RequestedSchema["properties"] is typically a -// map[string]rpc.UIElicitationSchemaProperty, but we accept any map[string]X. -func schemaHasProperty(schema map[string]any, name string) bool { +// schemaHasProperty reports whether the elicitation schema has a top-level +// property with the given name. +func schemaHasProperty(schema *copilot.ElicitationSchema, name string) bool { if schema == nil { return false } - props, ok := schema["properties"] - if !ok || props == nil { - return false - } - switch p := props.(type) { - case map[string]any: - _, found := p[name] - return found - case map[string]rpc.UIElicitationSchemaProperty: - _, found := p[name] - return found - default: - // Fallback: marshal/unmarshal via reflection-friendly route. - // For test diagnostic purposes we treat unknown shapes as not found. - return false - } + _, found := schema.Properties[name] + return found } func TestUIElicitationMultiClientE2E(t *testing.T) { @@ -776,7 +760,7 @@ func TestUIElicitationMultiClientE2E(t *testing.T) { OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SuppressResumeEvent: true, OnElicitationRequest: func(ctx copilot.ElicitationContext) (copilot.ElicitationResult, error) { - return copilot.ElicitationResult{Action: "accept", Content: map[string]any{}}, nil + return copilot.ElicitationResult{Action: copilot.ElicitationActionAccept, Content: map[string]any{}}, nil }, }) if err != nil { @@ -836,7 +820,7 @@ func TestUIElicitationMultiClientE2E(t *testing.T) { OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SuppressResumeEvent: true, OnElicitationRequest: func(ctx copilot.ElicitationContext) (copilot.ElicitationResult, error) { - return copilot.ElicitationResult{Action: "accept", Content: map[string]any{}}, nil + return copilot.ElicitationResult{Action: copilot.ElicitationActionAccept, Content: map[string]any{}}, nil }, }) if err != nil { diff --git a/go/internal/e2e/mcp_and_agents_e2e_test.go b/go/internal/e2e/mcp_and_agents_e2e_test.go index dd2ff228b..e4bd34e26 100644 --- a/go/internal/e2e/mcp_and_agents_e2e_test.go +++ b/go/internal/e2e/mcp_and_agents_e2e_test.go @@ -59,7 +59,7 @@ func TestMCPServersE2E(t *testing.T) { mcpServers := map[string]copilot.MCPServerConfig{ "test-server": copilot.MCPStdioServerConfig{ Command: "git", - Tools: &[]string{"*"}, + Tools: []string{"*"}, }, } @@ -125,7 +125,7 @@ func TestMCPServersE2E(t *testing.T) { "env-echo": copilot.MCPStdioServerConfig{ Command: "node", Args: []string{mcpServerPath}, - Tools: &[]string{"*"}, + Tools: []string{"*"}, Env: map[string]string{"TEST_SECRET": "hunter2"}, WorkingDirectory: mcpServerDir, }, diff --git a/go/internal/e2e/mcp_server_helpers_test.go b/go/internal/e2e/mcp_server_helpers_test.go index 54c75e08c..1860067ed 100644 --- a/go/internal/e2e/mcp_server_helpers_test.go +++ b/go/internal/e2e/mcp_server_helpers_test.go @@ -23,7 +23,7 @@ func testMCPServers(t *testing.T, serverNames ...string) map[string]copilot.MCPS mcpServers[serverName] = copilot.MCPStdioServerConfig{ Command: "node", Args: []string{mcpServerPath}, - Tools: &[]string{"*"}, + Tools: []string{"*"}, WorkingDirectory: mcpServerDir, } } diff --git a/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go b/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go index 6dfb5b1b7..184727092 100644 --- a/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go +++ b/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go @@ -19,13 +19,12 @@ func TestPreMCPToolCallHookE2E(t *testing.T) { metaEchoServer := filepath.Join(testHarnessDir, "test-mcp-meta-echo-server.mjs") metaEchoConfig := func() map[string]copilot.MCPServerConfig { - tools := []string{"*"} return map[string]copilot.MCPServerConfig{ "meta-echo": copilot.MCPStdioServerConfig{ Command: "node", Args: []string{metaEchoServer}, WorkingDirectory: testHarnessDir, - Tools: &tools, + Tools: []string{"*"}, }, } } diff --git a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go index cbaa92f9e..60ae3f1fd 100644 --- a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go +++ b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go @@ -311,7 +311,7 @@ func TestRPCTasksAndHandlersE2E(t *testing.T) { OnElicitationRequest: func(ctx copilot.ElicitationContext) (copilot.ElicitationResult, error) { handlerContext <- ctx return copilot.ElicitationResult{ - Action: "accept", + Action: copilot.ElicitationActionAccept, Content: map[string]any{ "answer": "from handler", "confirmed": true, @@ -347,7 +347,7 @@ func TestRPCTasksAndHandlersE2E(t *testing.T) { if ctx.SessionID != session.SessionID || ctx.Message != "Need details" { t.Fatalf("Unexpected elicitation context: %+v", ctx) } - if _, ok := ctx.RequestedSchema["properties"]; !ok { + if ctx.RequestedSchema == nil || ctx.RequestedSchema.Properties == nil { t.Fatalf("Expected requested schema to include properties, got %+v", ctx.RequestedSchema) } if response.Action != rpc.UIElicitationResponseActionAccept { diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 730d0f9fb..d6ae99b6c 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -2204,14 +2204,14 @@ type MCPServer struct { // Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. type MCPServerAuthConfig interface { - mCPServerAuthConfig() + mcpServerAuthConfig() } type MCPServerAuthConfigBoolean bool -func (MCPServerAuthConfigBoolean) mCPServerAuthConfig() {} +func (MCPServerAuthConfigBoolean) mcpServerAuthConfig() {} -func (MCPServerAuthConfigRedirectPort) mCPServerAuthConfig() {} +func (MCPServerAuthConfigRedirectPort) mcpServerAuthConfig() {} // Authentication settings with optional redirect port configuration. type MCPServerAuthConfigRedirectPort struct { @@ -2221,14 +2221,14 @@ type MCPServerAuthConfigRedirectPort struct { // MCP server configuration (stdio process or remote HTTP/SSE) type MCPServerConfig interface { - mCPServerConfig() + mcpServerConfig() } type RawMCPServerConfigData struct { Raw json.RawMessage } -func (RawMCPServerConfigData) mCPServerConfig() {} +func (RawMCPServerConfigData) mcpServerConfig() {} // Remote MCP server configuration accessed over HTTP or SSE. type MCPServerConfigHTTP struct { @@ -2260,7 +2260,7 @@ type MCPServerConfigHTTP struct { URL string `json:"url"` } -func (MCPServerConfigHTTP) mCPServerConfig() {} +func (MCPServerConfigHTTP) mcpServerConfig() {} // Stdio MCP server configuration launched as a child process. type MCPServerConfigStdio struct { @@ -2288,7 +2288,7 @@ type MCPServerConfigStdio struct { Tools []string `json:"tools,omitzero"` } -func (MCPServerConfigStdio) mCPServerConfig() {} +func (MCPServerConfigStdio) mcpServerConfig() {} // MCP servers configured for the session, with their connection status. // Experimental: MCPServerList is part of an experimental API and may change or be removed. @@ -6115,24 +6115,24 @@ type UIElicitationArrayEnumFieldItems struct { // Experimental: UIElicitationFieldValue is part of an experimental API and may change or be // removed. type UIElicitationFieldValue interface { - uIElicitationFieldValue() + uiElicitationFieldValue() } type UIElicitationBooleanValue bool -func (UIElicitationBooleanValue) uIElicitationFieldValue() {} +func (UIElicitationBooleanValue) uiElicitationFieldValue() {} type UIElicitationNumberValue float64 -func (UIElicitationNumberValue) uIElicitationFieldValue() {} +func (UIElicitationNumberValue) uiElicitationFieldValue() {} type UIElicitationStringArrayValue []string -func (UIElicitationStringArrayValue) uIElicitationFieldValue() {} +func (UIElicitationStringArrayValue) uiElicitationFieldValue() {} type UIElicitationStringValue string -func (UIElicitationStringValue) uIElicitationFieldValue() {} +func (UIElicitationStringValue) uiElicitationFieldValue() {} // Prompt message and JSON schema describing the form fields to elicit from the user. // Experimental: UIElicitationRequest is part of an experimental API and may change or be @@ -6185,7 +6185,7 @@ type UIElicitationSchema struct { // Experimental: UIElicitationSchemaProperty is part of an experimental API and may change // or be removed. type UIElicitationSchemaProperty interface { - uIElicitationSchemaProperty() + uiElicitationSchemaProperty() Type() UIElicitationSchemaPropertyType } @@ -6194,7 +6194,7 @@ type RawUIElicitationSchemaPropertyData struct { Raw json.RawMessage } -func (RawUIElicitationSchemaPropertyData) uIElicitationSchemaProperty() {} +func (RawUIElicitationSchemaPropertyData) uiElicitationSchemaProperty() {} func (r RawUIElicitationSchemaPropertyData) Type() UIElicitationSchemaPropertyType { return r.Discriminator } @@ -6217,7 +6217,7 @@ type UIElicitationArrayAnyOfField struct { Title *string `json:"title,omitempty"` } -func (UIElicitationArrayAnyOfField) uIElicitationSchemaProperty() {} +func (UIElicitationArrayAnyOfField) uiElicitationSchemaProperty() {} func (UIElicitationArrayAnyOfField) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeArray } @@ -6240,7 +6240,7 @@ type UIElicitationArrayEnumField struct { Title *string `json:"title,omitempty"` } -func (UIElicitationArrayEnumField) uIElicitationSchemaProperty() {} +func (UIElicitationArrayEnumField) uiElicitationSchemaProperty() {} func (UIElicitationArrayEnumField) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeArray } @@ -6257,7 +6257,7 @@ type UIElicitationSchemaPropertyBoolean struct { Title *string `json:"title,omitempty"` } -func (UIElicitationSchemaPropertyBoolean) uIElicitationSchemaProperty() {} +func (UIElicitationSchemaPropertyBoolean) uiElicitationSchemaProperty() {} func (UIElicitationSchemaPropertyBoolean) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeBoolean } @@ -6279,7 +6279,7 @@ type UIElicitationSchemaPropertyNumber struct { Discriminator UIElicitationSchemaPropertyNumberType `json:"type,omitempty"` } -func (UIElicitationSchemaPropertyNumber) uIElicitationSchemaProperty() {} +func (UIElicitationSchemaPropertyNumber) uiElicitationSchemaProperty() {} func (r UIElicitationSchemaPropertyNumber) Type() UIElicitationSchemaPropertyType { if r.Discriminator == "" { return UIElicitationSchemaPropertyTypeNumber @@ -6305,7 +6305,7 @@ type UIElicitationSchemaPropertyString struct { Title *string `json:"title,omitempty"` } -func (UIElicitationSchemaPropertyString) uIElicitationSchemaProperty() {} +func (UIElicitationSchemaPropertyString) uiElicitationSchemaProperty() {} func (UIElicitationSchemaPropertyString) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeString } @@ -6326,7 +6326,7 @@ type UIElicitationStringEnumField struct { Title *string `json:"title,omitempty"` } -func (UIElicitationStringEnumField) uIElicitationSchemaProperty() {} +func (UIElicitationStringEnumField) uiElicitationSchemaProperty() {} func (UIElicitationStringEnumField) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeString } @@ -6345,7 +6345,7 @@ type UIElicitationStringOneOfField struct { Title *string `json:"title,omitempty"` } -func (UIElicitationStringOneOfField) uIElicitationSchemaProperty() {} +func (UIElicitationStringOneOfField) uiElicitationSchemaProperty() {} func (UIElicitationStringOneOfField) Type() UIElicitationSchemaPropertyType { return UIElicitationSchemaPropertyTypeString } diff --git a/go/session.go b/go/session.go index fadf338b7..b103ed4a4 100644 --- a/go/session.go +++ b/go/session.go @@ -867,25 +867,28 @@ func (s *Session) handleElicitationRequest(elicitCtx ElicitationContext, request return } - rpcContent := make(map[string]rpc.UIElicitationFieldValue) - for k, v := range result.Content { - contentValue, err := toRPCContent(v) - if err != nil { - s.RPC.UI.HandlePendingElicitation(ctx, &rpc.UIHandlePendingElicitationRequest{ - RequestID: requestID, - Result: rpc.UIElicitationResponse{ - Action: rpc.UIElicitationResponseActionCancel, - }, - }) - return + var rpcContent map[string]rpc.UIElicitationFieldValue + if result.Content != nil { + rpcContent = make(map[string]rpc.UIElicitationFieldValue, len(result.Content)) + for k, v := range result.Content { + contentValue, err := toRPCContent(v) + if err != nil { + s.RPC.UI.HandlePendingElicitation(ctx, &rpc.UIHandlePendingElicitationRequest{ + RequestID: requestID, + Result: rpc.UIElicitationResponse{ + Action: rpc.UIElicitationResponseActionCancel, + }, + }) + return + } + rpcContent[k] = contentValue } - rpcContent[k] = contentValue } s.RPC.UI.HandlePendingElicitation(ctx, &rpc.UIHandlePendingElicitationRequest{ RequestID: requestID, Result: rpc.UIElicitationResponse{ - Action: rpc.UIElicitationResponseAction(result.Action), + Action: result.Action, Content: rpcContent, }, }) @@ -894,7 +897,7 @@ func (s *Session) handleElicitationRequest(elicitCtx ElicitationContext, request // toRPCContent converts an SDK content value to an RPC elicitation response value. func toRPCContent(v any) (rpc.UIElicitationFieldValue, error) { if v == nil { - return nil, nil + return nil, fmt.Errorf("unsupported elicitation content value type ") } switch val := v.(type) { case bool: @@ -983,13 +986,17 @@ func (s *Session) assertElicitation() error { } // Elicitation shows a generic elicitation dialog with a custom schema. -func (ui *SessionUI) Elicitation(ctx context.Context, message string, requestedSchema rpc.UIElicitationSchema) (*ElicitationResult, error) { +func (ui *SessionUI) Elicitation(ctx context.Context, message string, requestedSchema ElicitationSchema) (*ElicitationResult, error) { if err := ui.session.assertElicitation(); err != nil { return nil, err } + rpcSchema, err := toRPCUIElicitationSchema(requestedSchema) + if err != nil { + return nil, err + } rpcResult, err := ui.session.RPC.UI.Elicitation(ctx, &rpc.UIElicitationRequest{ Message: message, - RequestedSchema: requestedSchema, + RequestedSchema: rpcSchema, }) if err != nil { return nil, err @@ -997,6 +1004,27 @@ func (ui *SessionUI) Elicitation(ctx context.Context, message string, requestedS return fromRPCElicitationResult(rpcResult), nil } +func toRPCUIElicitationSchema(schema ElicitationSchema) (rpc.UIElicitationSchema, error) { + type wireSchema struct { + Properties map[string]any `json:"properties"` + Required []string `json:"required,omitzero"` + Type rpc.UIElicitationSchemaType `json:"type"` + } + data, err := json.Marshal(wireSchema{ + Properties: schema.Properties, + Required: schema.Required, + Type: rpc.UIElicitationSchemaTypeObject, + }) + if err != nil { + return rpc.UIElicitationSchema{}, err + } + var rpcSchema rpc.UIElicitationSchema + if err := json.Unmarshal(data, &rpcSchema); err != nil { + return rpc.UIElicitationSchema{}, err + } + return rpcSchema, nil +} + // Confirm shows a confirmation dialog and returns the user's boolean answer. // Returns false if the user declines or cancels. func (ui *SessionUI) Confirm(ctx context.Context, message string) (bool, error) { @@ -1111,17 +1139,20 @@ func fromRPCElicitationResult(r *rpc.UIElicitationResponse) *ElicitationResult { if r == nil { return nil } - content := make(map[string]any) - for k, v := range r.Content { - content[k] = fromRPCContent(v) + var content map[string]ElicitationFieldValue + if r.Content != nil { + content = make(map[string]ElicitationFieldValue, len(r.Content)) + for k, v := range r.Content { + content[k] = fromRPCContent(v) + } } return &ElicitationResult{ - Action: string(r.Action), + Action: r.Action, Content: content, } } -func fromRPCContent(value rpc.UIElicitationFieldValue) any { +func fromRPCContent(value rpc.UIElicitationFieldValue) ElicitationFieldValue { switch v := value.(type) { case nil: return nil @@ -1137,6 +1168,16 @@ func fromRPCContent(value rpc.UIElicitationFieldValue) any { return nil } +func fromRPCElicitationRequestedSchema(schema *rpc.ElicitationRequestedSchema) *ElicitationSchema { + if schema == nil { + return nil + } + return &ElicitationSchema{ + Properties: schema.Properties, + Required: schema.Required, + } +} + // dispatchEvent enqueues an event for delivery to user handlers and fires // broadcast handlers concurrently. // @@ -1225,35 +1266,13 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { if handler == nil { return } - var requestedSchema map[string]any - if d.RequestedSchema != nil { - requestedSchema = map[string]any{ - "type": string(d.RequestedSchema.Type), - "properties": d.RequestedSchema.Properties, - } - if len(d.RequestedSchema.Required) > 0 { - requestedSchema["required"] = d.RequestedSchema.Required - } - } - mode := "" - if d.Mode != nil { - mode = string(*d.Mode) - } - elicitationSource := "" - if d.ElicitationSource != nil { - elicitationSource = *d.ElicitationSource - } - url := "" - if d.URL != nil { - url = *d.URL - } s.handleElicitationRequest(ElicitationContext{ SessionID: s.SessionID, Message: d.Message, - RequestedSchema: requestedSchema, - Mode: mode, - ElicitationSource: elicitationSource, - URL: url, + RequestedSchema: fromRPCElicitationRequestedSchema(d.RequestedSchema), + Mode: d.Mode, + ElicitationSource: d.ElicitationSource, + URL: d.URL, }, d.RequestID) case *CapabilitiesChangedData: diff --git a/go/session_test.go b/go/session_test.go index 9e95e9a50..31e4f7fe0 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -718,7 +718,7 @@ func TestSession_ElicitationHandler(t *testing.T) { } session.registerElicitationHandler(func(ctx ElicitationContext) (ElicitationResult, error) { - return ElicitationResult{Action: "accept"}, nil + return ElicitationResult{Action: ElicitationActionAccept}, nil }) if session.getElicitationHandler() == nil { @@ -756,7 +756,7 @@ func TestSession_ElicitationHandler(t *testing.T) { session.registerElicitationHandler(func(ctx ElicitationContext) (ElicitationResult, error) { return ElicitationResult{ - Action: "accept", + Action: ElicitationActionAccept, Content: map[string]any{"color": "blue"}, }, nil }) @@ -768,7 +768,7 @@ func TestSession_ElicitationHandler(t *testing.T) { if err != nil { t.Fatalf("Expected no error, got %v", err) } - if result.Action != "accept" { + if result.Action != ElicitationActionAccept { t.Errorf("Expected action 'accept', got %q", result.Action) } if result.Content["color"] != "blue" { @@ -892,28 +892,20 @@ func TestSession_ElicitationRequestSchema(t *testing.T) { } required := []string{"name", "age"} - // Replicate the schema extraction logic from handleBroadcastEvent - requestedSchema := map[string]any{ - "type": "object", - "properties": properties, - } - if len(required) > 0 { - requestedSchema["required"] = required + requestedSchema := ElicitationSchema{ + Properties: properties, + Required: required, } - if requestedSchema["type"] != "object" { - t.Errorf("Expected schema type 'object', got %v", requestedSchema["type"]) - } - props, ok := requestedSchema["properties"].(map[string]any) - if !ok || props == nil { + props := requestedSchema.Properties + if props == nil { t.Fatal("Expected schema properties map") } if len(props) != 2 { t.Errorf("Expected 2 properties, got %d", len(props)) } - req, ok := requestedSchema["required"].([]string) - if !ok || len(req) != 2 { - t.Errorf("Expected required [name, age], got %v", requestedSchema["required"]) + if len(requestedSchema.Required) != 2 { + t.Errorf("Expected required [name, age], got %v", requestedSchema.Required) } }) @@ -922,18 +914,31 @@ func TestSession_ElicitationRequestSchema(t *testing.T) { "optional_field": map[string]any{"type": "string"}, } - requestedSchema := map[string]any{ - "type": "object", - "properties": properties, + requestedSchema := ElicitationSchema{ + Properties: properties, } - // Simulate: if len(schema.Required) > 0 { ... } — with empty required - var required []string - if len(required) > 0 { - requestedSchema["required"] = required + + if requestedSchema.Required != nil { + t.Error("Expected Required to be nil when omitted") + } + }) + + t.Run("schema conversion adds object type", func(t *testing.T) { + requestedSchema := ElicitationSchema{ + Properties: map[string]any{ + "name": map[string]any{"type": "string"}, + }, } - if _, exists := requestedSchema["required"]; exists { - t.Error("Expected no 'required' key when Required is empty") + rpcSchema, err := toRPCUIElicitationSchema(requestedSchema) + if err != nil { + t.Fatalf("toRPCUIElicitationSchema failed: %v", err) + } + if rpcSchema.Type != rpc.UIElicitationSchemaTypeObject { + t.Errorf("Expected RPC schema type object, got %q", rpcSchema.Type) + } + if _, ok := rpcSchema.Properties["name"].(*rpc.UIElicitationSchemaPropertyString); !ok { + t.Fatalf("Expected name property to decode as string schema, got %T", rpcSchema.Properties["name"]) } }) } diff --git a/go/types.go b/go/types.go index a944a452b..ce6298afa 100644 --- a/go/types.go +++ b/go/types.go @@ -743,19 +743,15 @@ type MCPServerConfig interface { // // The Tools field controls which tools from the server are exposed: // - nil (omitted from the wire): all tools (CLI default) -// - &[]string{"*"}: explicit "all tools" -// - &[]string{}: no tools -// - &[]string{"foo","bar"}: only those tools -// -// The pointer-to-slice form is required so that a nil pointer (omitted from -// the wire) is distinguishable from a non-nil pointer to an empty slice -// (sent as `"tools": []`). +// - []string{"*"}: explicit "all tools" +// - []string{}: no tools +// - []string{"foo","bar"}: only those tools type MCPStdioServerConfig struct { - Tools *[]string `json:"tools,omitempty"` + Tools []string `json:"tools,omitzero"` Timeout int `json:"timeout,omitempty"` Command string `json:"command"` - Args []string `json:"args,omitempty"` - Env map[string]string `json:"env,omitempty"` + Args []string `json:"args,omitzero"` + Env map[string]string `json:"env,omitzero"` WorkingDirectory string `json:"cwd,omitempty"` } @@ -777,10 +773,10 @@ func (c MCPStdioServerConfig) MarshalJSON() ([]byte, error) { // // See [MCPStdioServerConfig] for the semantics of the Tools field. type MCPHTTPServerConfig struct { - Tools *[]string `json:"tools,omitempty"` + Tools []string `json:"tools,omitzero"` Timeout int `json:"timeout,omitempty"` URL string `json:"url"` - Headers map[string]string `json:"headers,omitempty"` + Headers map[string]string `json:"headers,omitzero"` } func (MCPHTTPServerConfig) mcpServerConfig() {} @@ -1197,12 +1193,34 @@ type UICapabilities struct { MCPApps bool `json:"mcpApps,omitempty"` } +// ElicitationAction is the user response to an elicitation request. +type ElicitationAction = rpc.UIElicitationResponseAction + +// Elicitation action values. +const ( + ElicitationActionAccept = rpc.UIElicitationResponseActionAccept + ElicitationActionCancel = rpc.UIElicitationResponseActionCancel + ElicitationActionDecline = rpc.UIElicitationResponseActionDecline +) + +// ElicitationFieldValue is a primitive value submitted for an elicitation form field. +// Supported values are string, numeric types, bool, []string, and []any containing strings. +type ElicitationFieldValue = any + // ElicitationResult is the user's response to an elicitation dialog. type ElicitationResult struct { - // Action is the user response: "accept" (submitted), "decline" (rejected), or "cancel" (dismissed). - Action string `json:"action"` - // Content holds form values submitted by the user (present when Action is "accept"). - Content map[string]any `json:"content,omitzero"` + // Action is the user response: accept, decline, or cancel. + Action ElicitationAction `json:"action"` + // Content holds form values submitted by the user when Action is accept. + Content map[string]ElicitationFieldValue `json:"content,omitzero"` +} + +// ElicitationSchema describes the form fields for an elicitation request. +type ElicitationSchema struct { + // Properties contains form field definitions keyed by field name. + Properties map[string]any `json:"properties"` + // Required lists field names that must be submitted. + Required []string `json:"required,omitzero"` } // ElicitationContext describes an elicitation request from the server, @@ -1214,13 +1232,13 @@ type ElicitationContext struct { // Message describes what information is needed from the user. Message string // RequestedSchema is a JSON Schema describing the form fields (form mode only). - RequestedSchema map[string]any + RequestedSchema *ElicitationSchema // Mode is "form" for structured input, "url" for browser redirect. - Mode string + Mode *ElicitationRequestedMode // ElicitationSource is the source that initiated the request (e.g. MCP server name). - ElicitationSource string + ElicitationSource *string // URL to open in the user's browser (url mode only). - URL string + URL *string } // ElicitationHandler handles elicitation requests from the server (e.g. from MCP tools). @@ -1548,7 +1566,7 @@ type ModelVisionLimits struct { // ModelLimits contains model limits type ModelLimits struct { MaxPromptTokens *int `json:"max_prompt_tokens,omitempty"` - MaxContextWindowTokens int `json:"max_context_window_tokens"` + MaxContextWindowTokens *int `json:"max_context_window_tokens,omitempty"` Vision *ModelVisionLimits `json:"vision,omitempty"` } diff --git a/go/types_test.go b/go/types_test.go index 4f536c53b..f4132fa3d 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -326,7 +326,7 @@ func TestCanvasDeclaration_JSONOmitsNilInputSchema(t *testing.T) { func TestElicitationResult_JSONIncludesEmptyContent(t *testing.T) { result := ElicitationResult{ - Action: "accept", + Action: ElicitationActionAccept, Content: map[string]any{}, } @@ -354,7 +354,7 @@ func TestElicitationResult_JSONIncludesEmptyContent(t *testing.T) { } func TestElicitationResult_JSONOmitsNilContent(t *testing.T) { - result := ElicitationResult{Action: "cancel"} + result := ElicitationResult{Action: ElicitationActionCancel} data, err := json.Marshal(result) if err != nil { diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 69556d094..82aac2a0b 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -115,6 +115,20 @@ function toGoFieldName(jsonName: string): string { .join(""); } +function toGoUnexportedIdentifier(name: string): string { + const leadingSpecialCases = [ + ...Array.from(goIdentifierCasingOverrides.values()), + ...Array.from(goInitialisms, (initialism) => initialism.toUpperCase()), + ].sort((left, right) => right.length - left.length); + + const leadingSpecialCase = leadingSpecialCases.find((specialCase) => name.startsWith(specialCase)); + if (leadingSpecialCase) { + return leadingSpecialCase.toLowerCase() + name.slice(leadingSpecialCase.length); + } + + return name.charAt(0).toLowerCase() + name.slice(1); +} + function goRefTypeName(ref: string, definitions?: DefinitionCollections, currentPackage?: string): string { const externalRef = parseExternalSchemaRef(ref); if (externalRef) { @@ -1754,7 +1768,7 @@ function emitGoFlatDiscriminatedUnion( const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); const rawDataName = `Raw${typeName}${ctx.discriminatedUnionRawVariantSuffix ?? "Data"}`; const hasRawVariant = discriminator.valueKind === "string"; - const markerName = `${typeName.charAt(0).toLowerCase()}${typeName.slice(1)}`; + const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); const lines: string[] = []; @@ -1952,7 +1966,7 @@ function emitGoRequiredFieldDiscriminatedUnion( const unionVariants = [...discriminator.variants].sort((left, right) => compareGoTypeNames(left.typeName, right.typeName)); const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); const rawDataName = `Raw${typeName}${ctx.discriminatedUnionRawVariantSuffix ?? "Data"}`; - const markerName = `${typeName.charAt(0).toLowerCase()}${typeName.slice(1)}`; + const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); const lines: string[] = []; @@ -2515,7 +2529,7 @@ function emitGoPrimitiveUnionInterface(typeName: string, schema: JSONSchema7, ct ctx.generatedNames.add(typeName); const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); - const markerName = `${typeName.charAt(0).toLowerCase()}${typeName.slice(1)}`; + const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); const lines: string[] = []; @@ -2671,7 +2685,7 @@ function emitGoUntaggedUnionInterface(typeName: string, schema: JSONSchema7, ctx ctx.generatedNames.add(typeName); const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); - const markerName = `${typeName.charAt(0).toLowerCase()}${typeName.slice(1)}`; + const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); const lines: string[] = []; From 7d4acd83be420e8e9893eb56fcca55b3a45f5d7a Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 2 Jun 2026 12:32:18 +0200 Subject: [PATCH 2/2] fix codereview comments --- go/session.go | 57 ++++++++++++++++++++++++++++++++++++---------- go/session_test.go | 23 +++++++++++++++++++ go/types.go | 6 ++--- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/go/session.go b/go/session.go index b103ed4a4..3e37a3483 100644 --- a/go/session.go +++ b/go/session.go @@ -897,7 +897,7 @@ func (s *Session) handleElicitationRequest(elicitCtx ElicitationContext, request // toRPCContent converts an SDK content value to an RPC elicitation response value. func toRPCContent(v any) (rpc.UIElicitationFieldValue, error) { if v == nil { - return nil, fmt.Errorf("unsupported elicitation content value type ") + return nil, nil } switch val := v.(type) { case bool: @@ -1005,24 +1005,57 @@ func (ui *SessionUI) Elicitation(ctx context.Context, message string, requestedS } func toRPCUIElicitationSchema(schema ElicitationSchema) (rpc.UIElicitationSchema, error) { - type wireSchema struct { - Properties map[string]any `json:"properties"` - Required []string `json:"required,omitzero"` - Type rpc.UIElicitationSchemaType `json:"type"` + var properties map[string]rpc.UIElicitationSchemaProperty + if schema.Properties != nil { + properties = make(map[string]rpc.UIElicitationSchemaProperty, len(schema.Properties)) + for name, property := range schema.Properties { + rpcProperty, err := toRPCUIElicitationSchemaProperty(name, property) + if err != nil { + return rpc.UIElicitationSchema{}, err + } + properties[name] = rpcProperty + } } - data, err := json.Marshal(wireSchema{ - Properties: schema.Properties, - Required: schema.Required, + + return rpc.UIElicitationSchema{ + Properties: properties, + Required: append([]string(nil), schema.Required...), + Type: rpc.UIElicitationSchemaTypeObject, + }, nil +} + +func toRPCUIElicitationSchemaProperty(name string, property any) (rpc.UIElicitationSchemaProperty, error) { + if property == nil { + return nil, fmt.Errorf("elicitation schema property %q is nil", name) + } + if rpcProperty, ok := property.(rpc.UIElicitationSchemaProperty); ok { + return rpcProperty, nil + } + + data, err := json.Marshal(property) + if err != nil { + return nil, fmt.Errorf("marshal elicitation schema property %q: %w", name, err) + } + wrapperData, err := json.Marshal(struct { + Properties map[string]json.RawMessage `json:"properties"` + Type rpc.UIElicitationSchemaType `json:"type"` + }{ + Properties: map[string]json.RawMessage{name: data}, Type: rpc.UIElicitationSchemaTypeObject, }) if err != nil { - return rpc.UIElicitationSchema{}, err + return nil, fmt.Errorf("marshal elicitation schema wrapper for property %q: %w", name, err) } + var rpcSchema rpc.UIElicitationSchema - if err := json.Unmarshal(data, &rpcSchema); err != nil { - return rpc.UIElicitationSchema{}, err + if err := json.Unmarshal(wrapperData, &rpcSchema); err != nil { + return nil, fmt.Errorf("decode elicitation schema property %q: %w", name, err) + } + rpcProperty, ok := rpcSchema.Properties[name] + if !ok { + return nil, fmt.Errorf("decode elicitation schema property %q: property missing after conversion", name) } - return rpcSchema, nil + return rpcProperty, nil } // Confirm shows a confirmation dialog and returns the user's boolean answer. diff --git a/go/session_test.go b/go/session_test.go index 31e4f7fe0..85bca1a05 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -883,6 +883,16 @@ func TestSession_HookForwardCompatibility(t *testing.T) { } func TestSession_ElicitationRequestSchema(t *testing.T) { + t.Run("nil content values are allowed", func(t *testing.T) { + value, err := toRPCContent(nil) + if err != nil { + t.Fatalf("Expected nil content to be accepted, got %v", err) + } + if value != nil { + t.Fatalf("Expected nil RPC content, got %T", value) + } + }) + t.Run("elicitation.requested passes full schema to handler", func(t *testing.T) { // Verify the schema extraction logic from handleBroadcastEvent // preserves type, properties, and required. @@ -941,4 +951,17 @@ func TestSession_ElicitationRequestSchema(t *testing.T) { t.Fatalf("Expected name property to decode as string schema, got %T", rpcSchema.Properties["name"]) } }) + + t.Run("schema conversion preserves typed properties", func(t *testing.T) { + property := &rpc.UIElicitationSchemaPropertyString{} + rpcSchema, err := toRPCUIElicitationSchema(ElicitationSchema{ + Properties: map[string]any{"name": property}, + }) + if err != nil { + t.Fatalf("toRPCUIElicitationSchema failed: %v", err) + } + if rpcSchema.Properties["name"] != property { + t.Fatalf("Expected typed property to be preserved, got %T", rpcSchema.Properties["name"]) + } + }) } diff --git a/go/types.go b/go/types.go index ce6298afa..7ffd454a3 100644 --- a/go/types.go +++ b/go/types.go @@ -1198,9 +1198,9 @@ type ElicitationAction = rpc.UIElicitationResponseAction // Elicitation action values. const ( - ElicitationActionAccept = rpc.UIElicitationResponseActionAccept - ElicitationActionCancel = rpc.UIElicitationResponseActionCancel - ElicitationActionDecline = rpc.UIElicitationResponseActionDecline + ElicitationActionAccept ElicitationAction = rpc.UIElicitationResponseActionAccept + ElicitationActionCancel ElicitationAction = rpc.UIElicitationResponseActionCancel + ElicitationActionDecline ElicitationAction = rpc.UIElicitationResponseActionDecline ) // ElicitationFieldValue is a primitive value submitted for an elicitation form field.