Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/extended_thinking/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func exampleThinkingConfig() {

// Use WithThinking for structured thinking configuration.
// ThinkingConfigEnabled sets an explicit token budget.
// ThinkingConfigAdaptive uses a default of 32,000 tokens.
// ThinkingConfigAdaptive enables adaptive thinking mode.
// ThinkingConfigDisabled turns off thinking entirely.
if err := client.Start(ctx,
claudesdk.WithLogger(logger),
Expand Down
12 changes: 12 additions & 0 deletions integration/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (

// TestToolPermissions_AllowExplicit tests CanUseTool returning PermissionResultAllow.
func TestToolPermissions_AllowExplicit(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
Expand Down Expand Up @@ -61,6 +63,8 @@ func TestToolPermissions_AllowExplicit(t *testing.T) {

// TestToolPermissions_Deny tests CanUseTool returning PermissionResultDeny.
func TestToolPermissions_Deny(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
Expand Down Expand Up @@ -105,6 +109,8 @@ func TestToolPermissions_Deny(t *testing.T) {

// TestToolPermissions_ModifyInput tests modifying tool input via UpdatedInput.
func TestToolPermissions_ModifyInput(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
Expand Down Expand Up @@ -212,6 +218,8 @@ func TestToolPermissions_SandboxNetworkAccess(t *testing.T) {

// TestToolPermissions_ClientInteractive tests CanUseTool through the interactive Client API.
func TestToolPermissions_ClientInteractive(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
Expand Down Expand Up @@ -306,6 +314,8 @@ func TestToolPermissions_ClientInteractive(t *testing.T) {
}

func TestToolPermissions_PersistAcrossSessions(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
Expand Down Expand Up @@ -396,6 +406,8 @@ func TestToolPermissions_PersistAcrossSessions(t *testing.T) {
}

func TestToolPermissions_PersistDeniedAcrossSessions(t *testing.T) {
// TODO: CLI v2.1.98+ does not thread --permission-prompt-tool into streaming mode options.
// These tests will fail until the CLI fix lands. See commit message for root cause analysis.
t.Setenv("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "5")

ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
Expand Down
13 changes: 6 additions & 7 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1068,11 +1068,10 @@ func TestBuildArgs_WithThinkingConfigAdaptive(t *testing.T) {

args := BuildArgs("test", options, false)

// Adaptive defaults to 32000
thinkingIdx := slices.Index(args, "--max-thinking-tokens")
require.NotEqual(t, -1, thinkingIdx, "Expected --max-thinking-tokens flag")
thinkingIdx := slices.Index(args, "--thinking")
require.NotEqual(t, -1, thinkingIdx, "Expected --thinking flag")
require.Less(t, thinkingIdx+1, len(args))
require.Equal(t, "32000", args[thinkingIdx+1])
require.Equal(t, "adaptive", args[thinkingIdx+1])
}

// TestBuildArgs_WithThinkingConfigEnabled tests enabled thinking config with budget.
Expand All @@ -1097,10 +1096,10 @@ func TestBuildArgs_WithThinkingConfigDisabled(t *testing.T) {

args := BuildArgs("test", options, false)

thinkingIdx := slices.Index(args, "--max-thinking-tokens")
require.NotEqual(t, -1, thinkingIdx, "Expected --max-thinking-tokens flag")
thinkingIdx := slices.Index(args, "--thinking")
require.NotEqual(t, -1, thinkingIdx, "Expected --thinking flag")
require.Less(t, thinkingIdx+1, len(args))
require.Equal(t, "0", args[thinkingIdx+1])
require.Equal(t, "disabled", args[thinkingIdx+1])
}

// TestBuildArgs_WithEffort tests the effort flag.
Expand Down
23 changes: 15 additions & 8 deletions internal/cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ func BuildArgs(
if options.Thinking != nil {
switch t := options.Thinking.(type) {
case config.ThinkingConfigAdaptive:
args = append(args, "--max-thinking-tokens", "32000")
args = append(args, "--thinking", "adaptive")
case config.ThinkingConfigEnabled:
args = append(args, "--max-thinking-tokens", strconv.Itoa(t.BudgetTokens))
case config.ThinkingConfigDisabled:
args = append(args, "--max-thinking-tokens", "0")
args = append(args, "--thinking", "disabled")
}
}

Expand Down Expand Up @@ -190,6 +190,11 @@ func BuildArgs(
args = append(args, "--resume", options.Resume)
}

// Session ID
if options.SessionID != "" {
args = append(args, "--session-id", options.SessionID)
}

// Fork session
if options.ForkSession {
args = append(args, "--fork-session")
Expand All @@ -198,13 +203,15 @@ func BuildArgs(
// Note: Agents are sent via the initialize control request, not CLI flags.
// This avoids platform-specific ARG_MAX limits for large agent definitions.

// Setting sources - always set this flag (can be empty)
sources := make([]string, len(options.SettingSources))
for i, s := range options.SettingSources {
sources[i] = string(s)
}
// Setting sources (only when non-empty, consistent with AllowedTools/Betas pattern)
if len(options.SettingSources) > 0 {
sources := make([]string, len(options.SettingSources))
for i, s := range options.SettingSources {
sources[i] = string(s)
}

args = append(args, "--setting-sources", strings.Join(sources, ","))
args = append(args, "--setting-sources", strings.Join(sources, ","))
}

// Plugins
for _, plugin := range options.Plugins {
Expand Down
84 changes: 84 additions & 0 deletions internal/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,87 @@ func TestBuildArgs_WithFastModeMergedIntoSettingsFile(t *testing.T) {

t.Fatal("expected merged --settings payload")
}

func TestBuildArgs_WithSessionID(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{SessionID: "sess-abc-123"}, false)

idx := slices.Index(args, "--session-id")
require.NotEqual(t, -1, idx, "Expected --session-id flag")
require.Equal(t, "sess-abc-123", args[idx+1])
}

func TestBuildArgs_SessionIDOmittedWhenEmpty(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{}, false)
require.NotContains(t, args, "--session-id")
}

func TestBuildArgs_SettingSourcesOmittedWhenEmpty(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{}, false)
require.NotContains(t, args, "--setting-sources")
}

func TestBuildArgs_SettingSourcesIncludedWhenSet(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{
SettingSources: []config.SettingSource{config.SettingSourceUser, config.SettingSourceProject},
}, false)

idx := slices.Index(args, "--setting-sources")
require.NotEqual(t, -1, idx, "Expected --setting-sources flag")
require.Equal(t, "user,project", args[idx+1])
}

func TestBuildArgs_ThinkingConfig(t *testing.T) {
t.Parallel()

tests := []struct {
name string
thinking config.ThinkingConfig
wantFlag string
wantValue string
}{
{
name: "adaptive uses --thinking adaptive",
thinking: config.ThinkingConfigAdaptive{},
wantFlag: "--thinking",
wantValue: "adaptive",
},
{
name: "enabled uses --max-thinking-tokens with budget",
thinking: config.ThinkingConfigEnabled{BudgetTokens: 16000},
wantFlag: "--max-thinking-tokens",
wantValue: "16000",
},
{
name: "disabled uses --thinking disabled",
thinking: config.ThinkingConfigDisabled{},
wantFlag: "--thinking",
wantValue: "disabled",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{Thinking: tt.thinking}, false)
idx := slices.Index(args, tt.wantFlag)
require.NotEqual(t, -1, idx, "Expected %s flag", tt.wantFlag)
require.Equal(t, tt.wantValue, args[idx+1])
})
}
}

func TestBuildArgs_ThinkingAdaptiveDoesNotUseMaxThinkingTokens(t *testing.T) {
t.Parallel()

args := BuildArgs("test", &config.Options{Thinking: config.ThinkingConfigAdaptive{}}, false)
require.NotContains(t, args, "--max-thinking-tokens")
}
4 changes: 3 additions & 1 deletion internal/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ type ThinkingConfig interface {
}

// ThinkingConfigAdaptive enables adaptive thinking mode.
// Uses a default budget of 32,000 tokens.
type ThinkingConfigAdaptive struct{}

func (ThinkingConfigAdaptive) thinkingConfig() {}
Expand Down Expand Up @@ -272,6 +271,9 @@ type Options struct {
// Resume is a session ID to resume from.
Resume string

// SessionID specifies an explicit session ID for this session.
SessionID string

// ForkSession indicates whether to fork the resumed session to a new ID.
ForkSession bool

Expand Down
7 changes: 4 additions & 3 deletions internal/config/preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,10 @@ type AgentDefinition struct {

// SystemPromptPreset defines a system prompt preset configuration.
type SystemPromptPreset struct {
Type string `json:"type"` // "preset"
Preset string `json:"preset"` // "claude_code"
Append *string `json:"append,omitempty"`
Type string `json:"type"` // "preset"
Preset string `json:"preset"` // "claude_code"
Append *string `json:"append,omitempty"`
ExcludeDynamicSections *bool `json:"excludeDynamicSections,omitempty"` // strip per-user dynamic sections for cross-user prompt caching
}

// PluginConfig configures a plugin to load.
Expand Down
23 changes: 19 additions & 4 deletions internal/mcp/sdk_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ type SDKServer struct {

// sdkTool holds tool metadata and handler for internal registry.
type sdkTool struct {
tool *mcp.Tool
handler mcp.ToolHandler
tool *mcp.Tool
handler mcp.ToolHandler
maxResultSizeChars *int
}

// NewSDKServer creates a new MCP SDK server wrapper.
Expand All @@ -42,12 +43,18 @@ func NewSDKServer(name, version string) *SDKServer {

// AddTool registers a tool with the server.
func (s *SDKServer) AddTool(tool *mcp.Tool, handler mcp.ToolHandler) {
s.AddToolWithOptions(tool, handler, nil)
}

// AddToolWithOptions registers a tool with optional metadata.
func (s *SDKServer) AddToolWithOptions(tool *mcp.Tool, handler mcp.ToolHandler, maxResultSizeChars *int) {
s.mu.Lock()
defer s.mu.Unlock()

s.tools[tool.Name] = &sdkTool{
tool: tool,
handler: handler,
tool: tool,
handler: handler,
maxResultSizeChars: maxResultSizeChars,
}
}

Expand Down Expand Up @@ -111,6 +118,14 @@ func (s *SDKServer) ListTools() []map[string]any {
}
}

// Add _meta for per-tool configuration (e.g., raised result size limit).
// This tells the CLI to raise the per-tool threshold beyond the default 50K char limit.
if t.maxResultSizeChars != nil {
toolMap["_meta"] = map[string]any{
"anthropic/maxResultSizeChars": *t.maxResultSizeChars,
}
}

result = append(result, toolMap)
}

Expand Down
37 changes: 37 additions & 0 deletions internal/mcp/sdk_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,43 @@ func TestConvertEmbeddedResourceToText(t *testing.T) {
})
}

func TestSDKServerListToolsWithMaxResultSizeChars(t *testing.T) {
server := NewSDKServer("demo", "1.0.0")
maxSize := 100000

server.AddToolWithOptions(
NewTool("big_tool", "returns large output", nil),
func(_ context.Context, _ *mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
return TextResult("ok"), nil
},
&maxSize,
)

tools := server.ListTools()
require.Len(t, tools, 1)

meta, ok := tools[0]["_meta"].(map[string]any)
require.True(t, ok, "_meta should be present")
require.Equal(t, 100000, meta["anthropic/maxResultSizeChars"])
}

func TestSDKServerListToolsWithoutMaxResultSizeChars(t *testing.T) {
server := NewSDKServer("demo", "1.0.0")

server.AddTool(
NewTool("normal_tool", "normal output", nil),
func(_ context.Context, _ *mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
return TextResult("ok"), nil
},
)

tools := server.ListTools()
require.Len(t, tools, 1)

_, hasMeta := tools[0]["_meta"]
require.False(t, hasMeta, "_meta should not be present when maxResultSizeChars is nil")
}

func strPtr(s string) *string {
return &s
}
1 change: 1 addition & 0 deletions internal/permission/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ type Context struct {
DisplayName *string
Description *string
AgentID *string
ToolUseID *string // Unique ID for this tool call
ToolCategory toolmeta.Category
ConcurrencySafe bool
InterruptBehavior toolmeta.InterruptBehavior
Expand Down
5 changes: 5 additions & 0 deletions internal/protocol/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ func (s *Session) Initialize(ctx context.Context) error {
if s.options.AgentProgressSummaries {
payload["agentProgressSummaries"] = true
}

if s.options.SystemPromptPreset != nil && s.options.SystemPromptPreset.ExcludeDynamicSections != nil {
payload["excludeDynamicSections"] = *s.options.SystemPromptPreset.ExcludeDynamicSections
}
}

timeout := s.getInitializeTimeout()
Expand Down Expand Up @@ -1181,6 +1185,7 @@ func parsePermissionContext(request map[string]any) *permission.Context {
DisplayName: permissionStringPtr(request, "display_name"),
Description: permissionStringPtr(request, "description"),
AgentID: permissionStringPtr(request, "agent_id"),
ToolUseID: permissionStringPtr(request, "tool_use_id"),
ToolCategory: toolmeta.Classify(toolName),
ConcurrencySafe: toolmeta.IsConcurrencySafe(toolName),
InterruptBehavior: toolmeta.InterruptMode(toolName),
Expand Down
2 changes: 2 additions & 0 deletions internal/protocol/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,8 @@ func TestSession_HandleCanUseTool_PermissionContextIncludesMetadata(t *testing.T
require.Equal(t, "Allow outbound network requests", *permCtx.Description)
require.NotNil(t, permCtx.AgentID)
require.Equal(t, "agent-123", *permCtx.AgentID)
require.NotNil(t, permCtx.ToolUseID)
require.Equal(t, "toolu_network_1", *permCtx.ToolUseID)
require.Equal(t, toolmeta.CategoryOther, permCtx.ToolCategory)
require.False(t, permCtx.ConcurrencySafe)
require.Equal(t, toolmeta.InterruptBehaviorBlock, permCtx.InterruptBehavior)
Expand Down
Loading
Loading