Summary
Add an optional MCP-tool-description compressor to sin-code serve (cmd/sin-code/internal/serve.go) that shrinks the tool manifest sent to MCP clients on every tools/list request, without changing the tool name, schema, or behavior. Inspired by JuliusBrussee/caveman's caveman-shrink npm-published MCP middleware (https://github.com/JuliusBrussee/caveman/blob/main/CLAUDE.md):
caveman-shrink — MCP middleware. Wraps any MCP server, compresses tool descriptions. npm.
For SIN-Code, this is a serve-time flag (--compress-tools) and a registered wrapper that the existing tool-registry can use. It targets the description field of every tool entry in registerAllMCPTools (serve.go:101-...) and any future tool the user adds.
Motivation
Caveman's caveman-shrink is the third-most-copied element in their README. The insight is that MCP clients (Claude Code, opencode, WebUI-v2) load the entire tool manifest on session start. With 44+ tools (current SIN-Code count, see AGENTS.md §6 and ECOSYSTEM.md), each with a verbose description and a multi-line schema, the manifest is a non-trivial fraction of every session's input tokens.
From the caveman README:
caveman-shrink — MCP middleware. Wraps any MCP server, compresses tool descriptions. npm.
The exact mechanism in caveman is:
- A thin Node MCP server that proxies another MCP server.
- On
tools/list, it forwards the request, then rewrites the response by:
- Trimming each
description to a one-liner.
- Collapsing the JSON schema's
description fields.
- Dropping optional properties that are rarely used.
- Preserving the
name, required, and the tool's behavior exactly.
SIN-Code's serve.go exposes 15+ tools. Each tool has a multi-line description (see serve.go:111-150 for sin_discover, sin_execute, sin_map). The descriptions are useful for the model, but they are also a tax on every session. A 40 % reduction in tool description bytes is a 40 % reduction in input tokens for every turn in every session that uses MCP.
Current State in SIN-Code
| Area |
File |
Lines |
What is there |
| MCP server entrypoint |
cmd/sin-code/internal/serve.go |
43–99 |
ServeCmd with --transport and --port flags. Calls registerAllMCPTools(server). |
| Tool registry |
cmd/sin-code/internal/serve.go |
101–... |
registerAllMCPTools is a hard-coded slice of toolDef literals. Each entry has name, description, schema, handler. |
| Tool handlers |
cmd/sin-code/internal/serve*.go |
— |
The 15+ handlers (handleDiscover, handleExecute, etc.) implement the tool behavior. The descriptions are written by hand. |
| Tool description length |
cmd/sin-code/internal/serve.go |
110–150 |
sin_discover description is "Discover files with relevance scoring, pattern matching, and dependency analysis". sin_execute is "Execute shell commands safely with secret redaction, timeout, and error analysis". |
| Tool list count |
cmd/sin-code/main.go |
— |
39 subcommands; the serve exposes 15+ as MCP tools. |
sin serve flag |
cmd/sin-code/internal/serve.go |
96–99 |
init() adds --transport and --port. No --compress-tools. |
What this tells us: SIN-Code has the tool registry. It does not have the wrap step that compresses the manifest on the way out. The wrap step is a small, well-bounded function in the mcp.NewServer callback.
What caveman does (precise)
From https://github.com/JuliusBrussee/caveman/blob/main/CLAUDE.md:
caveman-shrink — MCP middleware
Wraps any MCP server, compresses tool descriptions. npm.
The npm package is caveman-shrink. The internal implementation is src/mcp-servers/caveman-shrink/.
The internal implementation (which we have to infer from the README and the published package, since it is in src/mcp-servers/ which the README does not show in full) does the following:
- Listens as an MCP server itself.
- On
tools/list to the upstream, returns a rewritten version.
- The rewrite trims
description to a one-liner, collapses schema description fields, drops optional rarely-used properties.
- The rewrite preserves
name, required, and behavior.
The caveman-shrink is registered in opencode.json exactly like any other MCP server, but it wraps the real one. So the user's opencode config has:
{
"mcp": {
"caveman-shrink": { "command": ["caveman-shrink"], ... },
"actual-tool": { "command": ["actual-tool"], ... }
}
}
And the opencode config is told to use caveman-shrink as the proxy. The proxy forwards everything except tools/list to the real server.
For SIN-Code, the natural shape is different because SIN-Code is the MCP server in serve. We can compress inline. The wrap step is a single function call between registerAllMCPTools(server) and server.Run(ctx, transport).
Detailed Implementation Plan
Phase 1 — The compressor
-
Create cmd/sin-code/internal/mcpcompress/compressor.go (new package). Public surface:
type CompressedTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]any `json:"inputSchema"`
}
func CompressTool(t mcp.Tool, opts Options) CompressedTool
type Options struct {
MaxDescriptionLen int // default 80
DropSchemaDesc bool // default true
DropOptionalProps []string // property names to drop from schemas
}
mcp.Tool is the go-sdk type from github.com/modelcontextprotocol/go-sdk/mcp.
-
Implement CompressTool:
- Trim
description to Options.MaxDescriptionLen chars, ending on a word boundary.
- For each property in
InputSchema.Properties, if Options.DropSchemaDesc is true, set the property's description to "".
- For each property in
Options.DropOptionalProps, remove the property from InputSchema.Properties (and from InputSchema.Required if present).
-
Implement a MCPProxyServer that wraps the real server. The proxy:
- Forwards all non-
tools/list requests to the real server.
- For
tools/list, fetches the list from the real server, runs CompressTool on each, and returns the compressed list.
- The proxy is a
mcp.Server (or a mcp.ClientSession wrapped as a mcp.Server).
Phase 2 — Server integration
-
Add --compress-tools flag in cmd/sin-code/internal/serve.go:96-99:
var serveCompressTools bool
init() {
ServeCmd.Flags().BoolVar(&serveCompressTools, "compress-tools", false,
"Compress MCP tool descriptions on the wire (saves input tokens per turn)")
}
-
In ServeCmd.RunE, when serveCompressTools is true, wrap the server:
if serveCompressTools {
server = mcpcompress.Wrap(server, mcpcompress.Options{...})
}
mcpcompress.Wrap returns a *mcp.Server that proxies and compresses.
-
Add serve.compress config keys in cmd/sin-code/internal/config.go:25-43:
ServeCompressTools bool `toml:"serve.compress_tools"`
ServeCompressMaxDesc int `toml:"serve.compress_max_description"`
ServeCompressDropProps []string `toml:"serve.compress_drop_optional_props"`
These are read at ServeCmd.RunE time and override the flags if set.
-
Add a TUI/WebUI toggle: the WebUI-v2 connection string already includes the serve flags. We add a compress_tools query param so the WebUI can ask for the compressed manifest without requiring a restart.
Phase 3 — Per-tool overrides
-
Allow per-tool compression overrides in the toolDef struct. A Compress field in toolDef lets a developer say "this tool's description is already terse, do not compress" or "this tool's schema should be fully preserved":
type toolDef struct {
name string
description string
schema map[string]any
handler func(...)
compress *mcpcompress.Options // nil = use the default
}
-
Audit the existing 15+ tool descriptions in serve.go and apply the compressor. For tools with hand-tuned descriptions (e.g., sin_ibd, sin_oracle), keep the original. For tools with generic descriptions (e.g., sin_discover), the compressor will shorten them.
-
Add a sin-code serve --tools-manifest flag that prints the compressed manifest to stdout. This is the equivalent of caveman's "what the model sees". Useful for debugging the compressor.
Phase 4 — Tests
-
Unit test for CompressTool:
- Trim a 200-char description to 80 chars on a word boundary.
- Drop schema
description fields.
- Drop an optional property from both
Properties and Required.
- Preserve
name and required exactly.
-
Unit test for MCPProxyServer:
- Forward a non-
tools/list request verbatim.
- Compress the response of a
tools/list request.
- Preserve error responses.
-
Integration test for sin-code serve --compress-tools:
- Start the serve binary with
--compress-tools.
- Send a
tools/list request via JSON-RPC over stdio.
- Assert the response is shorter than the un-compressed version.
- Assert the tool names match exactly.
-
Race test: go test -race -count=1 ./cmd/sin-code/internal/mcpcompress/... must pass.
Phase 5 — Docs
docs/mcp-tool-compression.md: the wrap step, the config keys, the per-tool overrides, the trade-offs.
- Update
AGENTS.md §6 to list the new internal/mcpcompress/ package.
- Update
README.md to mention --compress-tools.
- Update
CHANGELOG.md Unreleased section.
Acceptance Criteria
Risk and Rollback
- Risk: aggressive compression hurts the model's ability to choose the right tool. Mitigation: the default
MaxDescriptionLen=80 is a soft cap; if the description is already under 80, no change. The schema description removal is opt-in via DropSchemaDesc=true; we ship that as default but the user can set it to false.
- Risk: the proxy introduces a network hop in HTTP mode. Mitigation: in HTTP mode, the proxy and the real server run in the same process; the "hop" is a function call.
- Risk: WebUI-v2 expects the full tool descriptions. Mitigation: the WebUI-v2 connection is opt-in for compression via the
compress_tools query param; the default is no compression.
- Rollback: revert the PR. The flag is opt-in; the default behavior is unchanged.
References
Summary
Add an optional MCP-tool-description compressor to
sin-code serve(cmd/sin-code/internal/serve.go) that shrinks the tool manifest sent to MCP clients on everytools/listrequest, without changing the tool name, schema, or behavior. Inspired byJuliusBrussee/caveman'scaveman-shrinknpm-published MCP middleware (https://github.com/JuliusBrussee/caveman/blob/main/CLAUDE.md):For SIN-Code, this is a serve-time flag (
--compress-tools) and a registered wrapper that the existing tool-registry can use. It targets thedescriptionfield of every tool entry inregisterAllMCPTools(serve.go:101-...) and any future tool the user adds.Motivation
Caveman's
caveman-shrinkis the third-most-copied element in their README. The insight is that MCP clients (Claude Code, opencode, WebUI-v2) load the entire tool manifest on session start. With 44+ tools (current SIN-Code count, seeAGENTS.md§6 andECOSYSTEM.md), each with a verbosedescriptionand a multi-lineschema, the manifest is a non-trivial fraction of every session's input tokens.From the caveman README:
The exact mechanism in caveman is:
tools/list, it forwards the request, then rewrites the response by:descriptionto a one-liner.descriptionfields.name,required, and the tool's behavior exactly.SIN-Code's
serve.goexposes 15+ tools. Each tool has a multi-linedescription(seeserve.go:111-150forsin_discover,sin_execute,sin_map). The descriptions are useful for the model, but they are also a tax on every session. A 40 % reduction in tool description bytes is a 40 % reduction in input tokens for every turn in every session that uses MCP.Current State in SIN-Code
cmd/sin-code/internal/serve.goServeCmdwith--transportand--portflags. CallsregisterAllMCPTools(server).cmd/sin-code/internal/serve.goregisterAllMCPToolsis a hard-coded slice oftoolDefliterals. Each entry hasname,description,schema,handler.cmd/sin-code/internal/serve*.gohandleDiscover,handleExecute, etc.) implement the tool behavior. The descriptions are written by hand.cmd/sin-code/internal/serve.gosin_discoverdescription is"Discover files with relevance scoring, pattern matching, and dependency analysis".sin_executeis"Execute shell commands safely with secret redaction, timeout, and error analysis".cmd/sin-code/main.goserveexposes 15+ as MCP tools.sin serveflagcmd/sin-code/internal/serve.goinit()adds--transportand--port. No--compress-tools.What this tells us: SIN-Code has the tool registry. It does not have the wrap step that compresses the manifest on the way out. The wrap step is a small, well-bounded function in the
mcp.NewServercallback.What caveman does (precise)
From
https://github.com/JuliusBrussee/caveman/blob/main/CLAUDE.md:The internal implementation (which we have to infer from the README and the published package, since it is in
src/mcp-servers/which the README does not show in full) does the following:tools/listto the upstream, returns a rewritten version.descriptionto a one-liner, collapses schemadescriptionfields, drops optional rarely-used properties.name,required, and behavior.The
caveman-shrinkis registered inopencode.jsonexactly like any other MCP server, but it wraps the real one. So the user's opencode config has:{ "mcp": { "caveman-shrink": { "command": ["caveman-shrink"], ... }, "actual-tool": { "command": ["actual-tool"], ... } } }And the opencode config is told to use
caveman-shrinkas the proxy. The proxy forwards everything excepttools/listto the real server.For SIN-Code, the natural shape is different because SIN-Code is the MCP server in
serve. We can compress inline. The wrap step is a single function call betweenregisterAllMCPTools(server)andserver.Run(ctx, transport).Detailed Implementation Plan
Phase 1 — The compressor
Create
cmd/sin-code/internal/mcpcompress/compressor.go(new package). Public surface:mcp.Toolis thego-sdktype fromgithub.com/modelcontextprotocol/go-sdk/mcp.Implement
CompressTool:descriptiontoOptions.MaxDescriptionLenchars, ending on a word boundary.InputSchema.Properties, ifOptions.DropSchemaDescis true, set the property'sdescriptionto"".Options.DropOptionalProps, remove the property fromInputSchema.Properties(and fromInputSchema.Requiredif present).Implement a
MCPProxyServerthat wraps the real server. The proxy:tools/listrequests to the real server.tools/list, fetches the list from the real server, runsCompressToolon each, and returns the compressed list.mcp.Server(or amcp.ClientSessionwrapped as amcp.Server).Phase 2 — Server integration
Add
--compress-toolsflag incmd/sin-code/internal/serve.go:96-99:In
ServeCmd.RunE, whenserveCompressToolsis true, wrap the server:mcpcompress.Wrapreturns a*mcp.Serverthat proxies and compresses.Add
serve.compressconfig keys incmd/sin-code/internal/config.go:25-43:These are read at
ServeCmd.RunEtime and override the flags if set.Add a TUI/WebUI toggle: the WebUI-v2 connection string already includes the
serveflags. We add acompress_toolsquery param so the WebUI can ask for the compressed manifest without requiring a restart.Phase 3 — Per-tool overrides
Allow per-tool compression overrides in the
toolDefstruct. ACompressfield intoolDeflets a developer say "this tool's description is already terse, do not compress" or "this tool's schema should be fully preserved":Audit the existing 15+ tool descriptions in
serve.goand apply the compressor. For tools with hand-tuned descriptions (e.g.,sin_ibd,sin_oracle), keep the original. For tools with generic descriptions (e.g.,sin_discover), the compressor will shorten them.Add a
sin-code serve --tools-manifestflag that prints the compressed manifest to stdout. This is the equivalent of caveman's "what the model sees". Useful for debugging the compressor.Phase 4 — Tests
Unit test for
CompressTool:descriptionfields.PropertiesandRequired.nameandrequiredexactly.Unit test for
MCPProxyServer:tools/listrequest verbatim.tools/listrequest.Integration test for
sin-code serve --compress-tools:--compress-tools.tools/listrequest via JSON-RPC over stdio.Race test:
go test -race -count=1 ./cmd/sin-code/internal/mcpcompress/...must pass.Phase 5 — Docs
docs/mcp-tool-compression.md: the wrap step, the config keys, the per-tool overrides, the trade-offs.AGENTS.md§6 to list the newinternal/mcpcompress/package.README.mdto mention--compress-tools.CHANGELOG.mdUnreleased section.Acceptance Criteria
cmd/sin-code/internal/mcpcompress/package exists withCompressToolandWrap.sin-code serve --compress-toolsworks for both stdio and http transports.len(json.Marshal(...))).sin-code serve --tools-manifestprints the compressed manifest.-race.golangci-lint,govulncheck,gosec(SARIF) green.Risk and Rollback
MaxDescriptionLen=80is a soft cap; if the description is already under 80, no change. The schemadescriptionremoval is opt-in viaDropSchemaDesc=true; we ship that as default but the user can set it to false.compress_toolsquery param; the default is no compression.References
caveman-shrink: https://github.com/JuliusBrussee/caveman/blob/main/CLAUDE.mdcmd/sin-code/internal/serve.go:43-99cmd/sin-code/internal/serve.go:101-...cmd/sin-code/internal/serve.go:86-93cmd/sin-code/internal/config.go:25-43