-
Notifications
You must be signed in to change notification settings - Fork 572
feat: add custom JSON unmarshaling for MCP duration fields and update config schema #2181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: graphite-base/2181
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,9 @@ package schemas | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
|
|
||
|
|
@@ -58,6 +60,83 @@ const ( | |
| DefaultToolExecutionTimeout = 30 * time.Second | ||
| ) | ||
|
|
||
| // parseMCPDurationJSON parses a JSON duration value that is either a Go duration | ||
| // string (e.g. "10m", "1h30m") or, as a fallback, an integer number of nanoseconds. | ||
| func parseMCPDurationJSON(raw json.RawMessage) (time.Duration, error) { | ||
| var s string | ||
| if sonic.Unmarshal(raw, &s) == nil { | ||
| return time.ParseDuration(s) | ||
| } | ||
| var n int64 | ||
| if err := sonic.Unmarshal(raw, &n); err != nil { | ||
| return 0, fmt.Errorf("expected Go duration string (e.g. \"10m\") or integer nanoseconds, got %s", string(raw)) | ||
| } | ||
| return time.Duration(n), nil | ||
| } | ||
|
|
||
| // UnmarshalJSON implements custom JSON decoding for MCPConfig, handling | ||
| // tool_sync_interval as a Go duration string (e.g. "10m", "1h"). | ||
| func (c *MCPConfig) UnmarshalJSON(data []byte) error { | ||
| type Alias MCPConfig | ||
| aux := &struct { | ||
| ToolSyncInterval json.RawMessage `json:"tool_sync_interval,omitempty"` | ||
| *Alias | ||
| }{Alias: (*Alias)(c)} | ||
| if err := sonic.Unmarshal(data, aux); err != nil { | ||
| return err | ||
| } | ||
| if len(aux.ToolSyncInterval) > 0 { | ||
| d, err := parseMCPDurationJSON(aux.ToolSyncInterval) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid tool_sync_interval: %w", err) | ||
| } | ||
| c.ToolSyncInterval = d | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // UnmarshalJSON implements custom JSON decoding for MCPClientConfig, handling | ||
| // tool_sync_interval as a Go duration string (e.g. "10m", "1h"). | ||
| func (c *MCPClientConfig) UnmarshalJSON(data []byte) error { | ||
| type Alias MCPClientConfig | ||
| aux := &struct { | ||
| ToolSyncInterval json.RawMessage `json:"tool_sync_interval,omitempty"` | ||
| *Alias | ||
| }{Alias: (*Alias)(c)} | ||
| if err := sonic.Unmarshal(data, aux); err != nil { | ||
| return err | ||
| } | ||
| if len(aux.ToolSyncInterval) > 0 { | ||
| d, err := parseMCPDurationJSON(aux.ToolSyncInterval) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid tool_sync_interval: %w", err) | ||
| } | ||
| c.ToolSyncInterval = d | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // UnmarshalJSON implements custom JSON decoding for MCPToolManagerConfig, handling | ||
| // tool_execution_timeout as an integer number of seconds (as documented in the schema). | ||
| func (c *MCPToolManagerConfig) UnmarshalJSON(data []byte) error { | ||
| type Alias MCPToolManagerConfig | ||
| aux := &struct { | ||
| ToolExecutionTimeout json.RawMessage `json:"tool_execution_timeout,omitempty"` | ||
| *Alias | ||
| }{Alias: (*Alias)(c)} | ||
| if err := sonic.Unmarshal(data, aux); err != nil { | ||
| return err | ||
| } | ||
| if len(aux.ToolExecutionTimeout) > 0 { | ||
| var seconds int64 | ||
| if err := sonic.Unmarshal(aux.ToolExecutionTimeout, &seconds); err != nil { | ||
| return fmt.Errorf("invalid tool_execution_timeout: expected integer (seconds): %w", err) | ||
| } | ||
| c.ToolExecutionTimeout = time.Duration(seconds) * time.Second | ||
| } | ||
|
Comment on lines
+130
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject negative Line 131-136 accepts negative integers and converts them into negative durations. That can cause immediate timeout/cancellation behavior and hard-to-debug failures; add a Suggested guard if len(aux.ToolExecutionTimeout) > 0 {
var seconds int64
if err := sonic.Unmarshal(aux.ToolExecutionTimeout, &seconds); err != nil {
return fmt.Errorf("invalid tool_execution_timeout: expected integer (seconds): %w", err)
}
+ if seconds < 0 {
+ return fmt.Errorf("invalid tool_execution_timeout: must be >= 0")
+ }
c.ToolExecutionTimeout = time.Duration(seconds) * time.Second
}🤖 Prompt for AI Agents |
||
| return nil | ||
| } | ||
|
Comment on lines
+77
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add matching These structs now have custom decode paths, but default marshaling will still emit As per coding guidelines "Custom time.Duration fields in config structs must use custom MarshalJSON/UnmarshalJSON to convert between nanoseconds (Go) and milliseconds (JSON)". 🤖 Prompt for AI Agents |
||
|
|
||
| // CodeModeBindingLevel defines how tools are exposed in the VFS for code execution | ||
| type CodeModeBindingLevel string | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 11212
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 433
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 3543
🏁 Script executed:
rg -n "MarshalJSON" core/schemas/mcp.goRepository: maximhq/bifrost
Length of output: 41
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 1983
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 3779
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 409
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 426
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 41
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 41
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 809
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 1124
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 928
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 41
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 200
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 1078
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 41
🏁 Script executed:
Repository: maximhq/bifrost
Length of output: 1877
Tool sync interval unit mismatch causes data loss in API round-trips.
The
parseMCPDurationJSONfunction interprets integer values as nanoseconds, but the UI sendstool_sync_intervalas minutes (confirmed by schema atui/lib/types/schemas.ts:854and form labels). When the form submits a value like10(minutes), Go interprets it astime.Duration(10)nanoseconds—effectively zero.The UI already has a workaround (
toolSyncIntervalToMinutes) that converts API responses from nanoseconds to minutes, but this creates a broken round-trip:Fix: Update
MCPClientConfig.UnmarshalJSONto handle integer values as minutes (not nanoseconds) when parsingtool_sync_interval. Alternatively, add correspondingMarshalJSONimplementations to serialize durations consistently and document the expected unit (minutes vs. nanoseconds) across the API boundary.Also note:
MCPToolManagerConfig.UnmarshalJSONcorrectly treats integer values as seconds fortool_execution_timeout—apply similar clarity fortool_sync_interval.🤖 Prompt for AI Agents