Reduce MCP token usage

This commit is contained in:
2026-05-29 13:16:05 +01:00
parent da46340a82
commit 51aac9f447
9 changed files with 453 additions and 137 deletions

View File

@@ -74,10 +74,10 @@ type ToolHost interface {
// Inspection.
ListProcesses(callerID, kindFilter string) []ProcessInfo
GetProcessStatus(callerID, processID string) (ProcessStatus, error)
GetProjectStatus(callerID string) (ProjectStatus, error)
GetProcessOutput(callerID, processID, mode string, sinceOffset int64) (ProcessOutput, error)
GetProcessRawOutput(callerID, processID string, sinceOffset int64) (RawOutput, error)
SearchOutput(callerID, processID, pattern, kind string, limit int) (SearchResult, error)
GetProjectStatus(callerID string, includeTools bool) (ProjectStatus, error)
GetProcessOutput(callerID string, args ProcessOutputArgs) (ProcessOutput, error)
GetProcessRawOutput(callerID string, args RawOutputArgs) (RawOutput, error)
SearchOutput(callerID string, args SearchOutputArgs) (SearchResult, error)
WaitForPattern(callerID, processID, pattern string, timeoutSeconds float64, scope string) (matched bool, snippet string, err error)
GetProcessPorts(callerID, processID string) ([]PortSighting, error)
@@ -98,13 +98,13 @@ type ToolHost interface {
// Scratchpads.
ScratchpadList() ([]scratchpad.Entry, error)
ScratchpadRead(name string) (content string, revision string, err error)
ScratchpadRead(args ScratchpadReadArgs) (ScratchpadReadResult, error)
ScratchpadWrite(name, content, expectedRevision string) (revision string, err error)
ScratchpadAppend(name, content string) error
ScratchpadDelete(name string) error
// Meta.
WhoAmI(callerID string) WhoAmI
WhoAmI(callerID string, includeTools bool) WhoAmI
Help(callerID, topic string) HelpResponse
}
@@ -157,6 +157,10 @@ type ProjectStatus struct {
Scratchpads []scratchpad.Entry `json:"scratchpads"`
}
type ProjectStatusArgs struct {
IncludeTools bool `json:"include_tools"`
}
// ProjectMeta is the project root info echoed in many payloads.
type ProjectMeta struct {
Path string `json:"path"`
@@ -166,23 +170,42 @@ type ProjectMeta struct {
// ProcessOutput is the get_process_output payload. SPEC §7 enriches
// the old read_output result with screen geometry + version.
type ProcessOutput struct {
Content string `json:"content"`
Mode string `json:"mode"`
NewOffset int64 `json:"new_offset,omitempty"`
ActiveScreen string `json:"active_screen,omitempty"`
Rows int `json:"rows,omitempty"`
Cols int `json:"cols,omitempty"`
Cursor Cursor `json:"cursor"`
IdleMS int64 `json:"idle_ms,omitempty"`
Status string `json:"status,omitempty"`
ScreenVersion int64 `json:"screen_version,omitempty"`
Content string `json:"content"`
Mode string `json:"mode"`
NewOffset int64 `json:"new_offset,omitempty"`
ActiveScreen string `json:"active_screen,omitempty"`
Rows int `json:"rows,omitempty"`
Cols int `json:"cols,omitempty"`
Cursor Cursor `json:"cursor"`
IdleMS int64 `json:"idle_ms,omitempty"`
Status string `json:"status,omitempty"`
ScreenVersion int64 `json:"screen_version,omitempty"`
ContentBytes int `json:"content_bytes,omitempty"`
Truncated bool `json:"truncated,omitempty"`
TruncatedBytes int `json:"truncated_bytes,omitempty"`
}
type ProcessOutputArgs struct {
ProcessID string `json:"process_id"`
Mode string `json:"mode"`
SinceOffset int64 `json:"since_offset"`
MaxBytes int `json:"max_bytes"`
}
// RawOutput is the get_process_raw_output payload — ANSI preserved.
type RawOutput struct {
Content string `json:"content"`
NewOffset int64 `json:"new_offset"`
Status string `json:"status,omitempty"`
Content string `json:"content"`
NewOffset int64 `json:"new_offset"`
Status string `json:"status,omitempty"`
ContentBytes int `json:"content_bytes,omitempty"`
Truncated bool `json:"truncated,omitempty"`
TruncatedBytes int `json:"truncated_bytes,omitempty"`
}
type RawOutputArgs struct {
ProcessID string `json:"process_id"`
SinceOffset int64 `json:"since_offset"`
MaxBytes int `json:"max_bytes"`
}
// SearchResult is search_output's payload.
@@ -191,6 +214,14 @@ type SearchResult struct {
Truncated bool `json:"truncated"`
}
type SearchOutputArgs struct {
ProcessID string `json:"process_id"`
Pattern string `json:"pattern"`
Kind string `json:"kind"`
Limit int `json:"limit"`
MaxBytes int `json:"max_bytes"`
}
type SearchMatch struct {
LineNo int `json:"line_no"`
Text string `json:"text"`
@@ -245,6 +276,7 @@ type TimerInfo struct {
ID string `json:"timer_id"`
Label string `json:"label,omitempty"`
Body string `json:"body,omitempty"`
BodyTruncated bool `json:"body_truncated,omitempty"`
Kind string `json:"kind"` // "delay" | "idle_any" | "idle_all"
Status string `json:"status"` // "pending" | "paused"
OwnerID string `json:"owner_process_id"`
@@ -281,13 +313,14 @@ type SpawnProcessArgs struct {
// SendInputArgs is the input shape for send_input — covers text /
// paste / key with the optional wait+tail tail-after-send.
type SendInputArgs struct {
ProcessID string `json:"process_id"`
Kind string `json:"kind"` // "text" | "paste" | "key"
Text string `json:"text"`
Key string `json:"key"`
Submit *bool `json:"submit"`
WaitMS int `json:"wait_ms"`
TailMode string `json:"tail_mode"` // "none" | "stream" | "grid"
ProcessID string `json:"process_id"`
Kind string `json:"kind"` // "text" | "paste" | "key"
Text string `json:"text"`
Key string `json:"key"`
Submit *bool `json:"submit"`
WaitMS int `json:"wait_ms"`
TailMode string `json:"tail_mode"` // "none" | "stream" | "grid"
TailMaxBytes int `json:"tail_max_bytes"`
}
// SendInputResult is the return shape of send_input.
@@ -306,6 +339,27 @@ type WhoAmI struct {
AvailableTools []string `json:"available_tools"`
}
type WhoAmIArgs struct {
IncludeTools bool `json:"include_tools"`
}
type ScratchpadReadArgs struct {
Name string `json:"name"`
Offset int `json:"offset"`
MaxBytes int `json:"max_bytes"`
}
type ScratchpadReadResult struct {
Content string `json:"content"`
Revision string `json:"revision"`
Offset int `json:"offset,omitempty"`
NextOffset int `json:"next_offset,omitempty"`
ContentBytes int `json:"content_bytes,omitempty"`
TotalBytes int `json:"total_bytes,omitempty"`
Truncated bool `json:"truncated,omitempty"`
TruncatedBytes int `json:"truncated_bytes,omitempty"`
}
// HelpResponse is the help return shape.
type HelpResponse struct {
Topic string `json:"topic"`
@@ -507,61 +561,51 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return st, 0, "", nil
case "get_project_status":
ps, err := h.GetProjectStatus(callerID)
var p ProjectStatusArgs
_ = unmarshalParamsOptional(params, &p)
ps, err := h.GetProjectStatus(callerID, p.IncludeTools)
if err != nil {
return mapToolError(err)
}
return ps, 0, "", nil
case "get_process_output":
var p struct {
ProcessID string `json:"process_id"`
Mode string `json:"mode"`
SinceOffset int64 `json:"since_offset"`
}
var p ProcessOutputArgs
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
if p.Mode == "" {
p.Mode = "grid"
}
out, err := h.GetProcessOutput(callerID, p.ProcessID, p.Mode, p.SinceOffset)
out, err := h.GetProcessOutput(callerID, p)
if err != nil {
return mapToolError(err)
}
return out, 0, "", nil
case "get_process_raw_output":
var p struct {
ProcessID string `json:"process_id"`
SinceOffset int64 `json:"since_offset"`
}
var p RawOutputArgs
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
out, err := h.GetProcessRawOutput(callerID, p.ProcessID, p.SinceOffset)
out, err := h.GetProcessRawOutput(callerID, p)
if err != nil {
return mapToolError(err)
}
return out, 0, "", nil
case "search_output":
var p struct {
ProcessID string `json:"process_id"`
Pattern string `json:"pattern"`
Kind string `json:"kind"`
Limit int `json:"limit"`
}
var p SearchOutputArgs
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
if p.Limit <= 0 {
p.Limit = 20
p.Limit = 10
}
if p.Kind == "" {
p.Kind = "rendered"
}
res, err := h.SearchOutput(callerID, p.ProcessID, p.Pattern, p.Kind, p.Limit)
res, err := h.SearchOutput(callerID, p)
if err != nil {
return mapToolError(err)
}
@@ -731,17 +775,15 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return entries, 0, "", nil
case "scratchpad_read":
var p struct {
Name string `json:"name"`
}
var p ScratchpadReadArgs
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
content, rev, err := h.ScratchpadRead(p.Name)
res, err := h.ScratchpadRead(p)
if err != nil {
return nil, codeInternal, err.Error(), nil
}
return map[string]any{"content": content, "revision": rev}, 0, "", nil
return res, 0, "", nil
case "scratchpad_write":
var p struct {
@@ -790,7 +832,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return map[string]any{"ok": true}, 0, "", nil
case "whoami":
return h.WhoAmI(callerID), 0, "", nil
var p WhoAmIArgs
_ = unmarshalParamsOptional(params, &p)
return h.WhoAmI(callerID, p.IncludeTools), 0, "", nil
case "help":
var p struct {