a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
176 lines
7.5 KiB
Go
176 lines
7.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
)
|
|
|
|
// sharedParamLegend documents the parameters that recur across most
|
|
// list-shaped tools ONCE, in the MCP server `instructions` (emitted per
|
|
// session at initialize). Per-tool schemas then carry a terse one-liner
|
|
// per shared parameter instead of repeating the same paragraph 10-50
|
|
// times — the single largest source of duplicated schema bytes in the
|
|
// cold tools/list. Every shared parameter still exists on every tool that
|
|
// accepted it; only its prose moved here.
|
|
const sharedParamLegend = `Shared parameters (recur across list-shaped tools; per-tool schemas keep a one-line gloss):
|
|
- format: wire format for the response — json (verbose default), gcx (GCX1, round-trippable, ~27% fewer tokens; the default for known coding-agent clients), or toon (compact tabular text, lossy). An explicit value always wins.
|
|
- max_bytes: cap the marshalled response at this many bytes; the longest list is trimmed and truncation metadata (_truncated_by_budget, _max_returned_*) rides on the response. Omit or 0 = no cap; pass a value to opt into a tighter budget, or max_bytes:0 to opt out entirely.
|
|
- cursor: opaque pagination token from a previous response's next_cursor; round-trip it verbatim to fetch the next page (do not parse it).
|
|
- fields: comma-separated sparse fieldset — keep only these columns on each result row. Pure size win, no priority drops.
|
|
- limit: maximum rows to return (per-tool default, usually 20-50). Prefer pagination over a very large limit.
|
|
- scope: the slug of a saved scope (see save_scope) whose repositories and paths narrow the matches.
|
|
- repo / project / workspace / ref: multi-repo filters — a repository prefix/path, a project name, a workspace slug (session-bound sessions may only name their own), or a reference tag. Default to the session's cwd-bound repo.`
|
|
|
|
// sharedParamRewrites maps a recurring parameter name to the terse gloss
|
|
// that replaces its verbose per-tool description once the full semantics
|
|
// live in the shared legend. A rewrite fires only when the existing
|
|
// description carries one of the discriminator tokens, so a same-named but
|
|
// semantically-different parameter (e.g. a diff `scope` of
|
|
// unstaged/staged/all, or the nav `cursor` = focused symbol) keeps its own
|
|
// prose. Order is irrelevant — each parameter is matched independently.
|
|
var sharedParamRewrites = []struct {
|
|
name string
|
|
mustHave []string // lower-cased discriminator tokens; any match qualifies
|
|
terse string
|
|
}{
|
|
{"format", []string{"gcx", "toon", "wire format"}, "Wire format: json|gcx|toon; see server instructions."},
|
|
{"max_bytes", []string{"byte"}, "Response byte cap (0/omit = none); see server instructions."},
|
|
{"cursor", []string{"pagination", "next_cursor", "next page"}, "Opaque pagination cursor from a prior next_cursor; see server instructions."},
|
|
{"fields", []string{"comma-separated"}, "Comma-separated sparse fieldset to keep; see server instructions."},
|
|
{"scope", []string{"saved scope"}, "Saved-scope slug narrowing repos/paths; see server instructions."},
|
|
{"repo", []string{"repositor"}, "Repository prefix/path filter (multi-repo); see server instructions."},
|
|
{"project", []string{"project"}, "Restrict to repositories in a project; see server instructions."},
|
|
{"workspace", []string{"workspace"}, "Workspace slug filter (session-bound); see server instructions."},
|
|
{"ref", []string{"reference"}, "Reference-tag repository filter; see server instructions."},
|
|
}
|
|
|
|
// agentParamCap is the target length for a parameter description on the
|
|
// lean `agent` surface. Above it, leanGloss keeps the first sentence (or a
|
|
// word-boundary cut) so the coding-agent tools/list stays inside its byte
|
|
// ceiling. The full prose is always one tools_search / `full` preset away.
|
|
const agentParamCap = 40
|
|
|
|
// leanGloss compacts a parameter description for the lean agent surface:
|
|
// keep a leading "(qualifier)" if present (it disambiguates kind-specific
|
|
// params), then the first sentence, capped at agentParamCap.
|
|
func leanGloss(desc string) string {
|
|
desc = strings.TrimSpace(desc)
|
|
if len(desc) <= agentParamCap {
|
|
return desc
|
|
}
|
|
if i := strings.IndexByte(desc, '.'); i > 0 && i+1 <= agentParamCap {
|
|
return desc[:i+1]
|
|
}
|
|
cut := agentParamCap
|
|
if sp := strings.LastIndexByte(desc[:cut], ' '); sp > agentParamCap/2 {
|
|
cut = sp
|
|
}
|
|
return strings.TrimRight(desc[:cut], " ,;:—-") + "…"
|
|
}
|
|
|
|
// agentDescCap caps a tool DESCRIPTION on the lean surface to its leading
|
|
// contract sentences, dropping trailing reference detail / examples (which
|
|
// stay in the `full` preset and tools_search). Kept generous (~5 sentences)
|
|
// so the "what it does + key caveat" contract + trust content survives —
|
|
// only the longest descriptions' reference tails are trimmed. 0 disables.
|
|
const agentDescCap = 600
|
|
|
|
// leanDescription keeps the leading sentences of a tool description up to
|
|
// agentDescCap, cutting on a sentence boundary so the contract stays whole.
|
|
func leanDescription(desc string) string {
|
|
if agentDescCap <= 0 || len(desc) <= agentDescCap {
|
|
return desc
|
|
}
|
|
cut := agentDescCap
|
|
if i := strings.LastIndex(desc[:cut], ". "); i > agentDescCap/2 {
|
|
return desc[:i+1]
|
|
}
|
|
if sp := strings.LastIndexByte(desc[:cut], ' '); sp > agentDescCap/2 {
|
|
cut = sp
|
|
}
|
|
return strings.TrimRight(desc[:cut], " ,;:—-") + "…"
|
|
}
|
|
|
|
// leanizeAgentTool returns a copy of the tool with every parameter
|
|
// description compacted to a lean gloss (and, when agentDescCap is set, the
|
|
// tool description trimmed to its leading contract sentences). It deep-
|
|
// copies the properties map (and each property object) so the server's
|
|
// shared, registered schema is never mutated — only this session's
|
|
// tools/list view is compacted.
|
|
func leanizeAgentTool(t mcp.Tool) mcp.Tool {
|
|
t.Description = leanDescription(t.Description)
|
|
props := t.InputSchema.Properties
|
|
if len(props) == 0 {
|
|
return t
|
|
}
|
|
out := make(map[string]any, len(props))
|
|
for name, raw := range props {
|
|
pm, ok := raw.(map[string]any)
|
|
if !ok {
|
|
out[name] = raw
|
|
continue
|
|
}
|
|
cp := make(map[string]any, len(pm))
|
|
for k, v := range pm {
|
|
cp[k] = v
|
|
}
|
|
if d, ok := cp["description"].(string); ok {
|
|
cp["description"] = leanGloss(d)
|
|
}
|
|
out[name] = cp
|
|
}
|
|
t.InputSchema.Properties = out
|
|
return t
|
|
}
|
|
|
|
// leanizeAgentTools maps leanizeAgentTool over a slice.
|
|
func leanizeAgentTools(tools []mcp.Tool) []mcp.Tool {
|
|
out := make([]mcp.Tool, len(tools))
|
|
for i := range tools {
|
|
out[i] = leanizeAgentTool(tools[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// compactSharedToolParams rewrites the recurring-parameter descriptions on
|
|
// one tool to their terse gloss, having moved the full semantics into the
|
|
// shared legend. Idempotent, allocation-light, and a no-op for any tool
|
|
// whose parameters don't match a discriminator — so bespoke same-named
|
|
// parameters are left untouched. Applied at registration to every tool, so
|
|
// the saving compounds across the whole surface (core and full shrink too).
|
|
func compactSharedToolParams(tool *mcp.Tool) {
|
|
props := tool.InputSchema.Properties
|
|
if len(props) == 0 {
|
|
return
|
|
}
|
|
for _, rw := range sharedParamRewrites {
|
|
raw, ok := props[rw.name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
pm, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
desc, _ := pm["description"].(string)
|
|
// Never inflate: some tools already carry a terse gloss shorter than
|
|
// the shared one-liner (e.g. a bare "json|gcx|toon" format hint).
|
|
if desc == "" || len(rw.terse) >= len(desc) {
|
|
continue
|
|
}
|
|
low := strings.ToLower(desc)
|
|
matched := false
|
|
for _, tok := range rw.mustHave {
|
|
if strings.Contains(low, tok) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
pm["description"] = rw.terse
|
|
}
|
|
}
|