Skip to content

feat(serve): add --compress-tools flag for MCP tool-description compression (caveman-shrink analog) #173

Description

@Delqhi

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:

  1. Listens as an MCP server itself.
  2. On tools/list to the upstream, returns a rewritten version.
  3. The rewrite trims description to a one-liner, collapses schema description fields, drops optional rarely-used properties.
  4. 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

  1. 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.

  2. 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).
  3. 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

  1. 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)")
    }
  2. 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.

  3. 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.

  4. 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

  1. 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
    }
  2. 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.

  3. 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

  1. 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.
  2. Unit test for MCPProxyServer:

    • Forward a non-tools/list request verbatim.
    • Compress the response of a tools/list request.
    • Preserve error responses.
  3. 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.
  4. Race test: go test -race -count=1 ./cmd/sin-code/internal/mcpcompress/... must pass.

Phase 5 — Docs

  1. docs/mcp-tool-compression.md: the wrap step, the config keys, the per-tool overrides, the trade-offs.
  2. Update AGENTS.md §6 to list the new internal/mcpcompress/ package.
  3. Update README.md to mention --compress-tools.
  4. Update CHANGELOG.md Unreleased section.

Acceptance Criteria

  • cmd/sin-code/internal/mcpcompress/ package exists with CompressTool and Wrap.
  • sin-code serve --compress-tools works for both stdio and http transports.
  • The compressed manifest is at least 20 % shorter than the un-compressed one (measured by len(json.Marshal(...))).
  • Tool names, schemas, and behaviors are unchanged.
  • Per-tool overrides work.
  • sin-code serve --tools-manifest prints the compressed manifest.
  • All unit and integration tests pass with -race.
  • golangci-lint, govulncheck, gosec (SARIF) green.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions