Tool Customization
Tool descriptions from third-party MCPs are written for human readers, not agents. A single get_file description may run 400 tokens of prose and caveats — multiplied across dozens of tools, that erodes the context budget fast. The customization layer lets you replace any tool's description and parameter docs with a tighter version, define entirely new composite tools backed by SLOP scripts, and share those customizations with your team.
Customization is managed through the customize_tools meta-tool, which exposes eight actions: set_override, remove_override, list_overrides, define_custom, remove_custom, list_custom, export, and import.
When to Customize
- A vendor MCP ships verbose, marketing-heavy tool descriptions that burn context on every search hit.
- Parameter docs include deprecation notices, edge-case caveats, or SDK history that an agent doesn't need.
- You want project-specific shortcuts — a single
deploy_stagingtool that wraps three github + terraform calls. - Your team has agreed on a set of description standards and wants them committed to the repo.
Scopes
Customizations are stored at one of three scopes. Higher-priority scopes shadow lower ones; the original MCP tool is never modified.
| Scope | Directory | Files | Commit to git? |
|---|---|---|---|
user | ~/.config/slop-mcp/memory/_slop/ | _slop.overrides.json, _slop.tools.json | No — personal |
project | <repo>/.slop-mcp/memory/_slop/ | _slop.overrides.json, _slop.tools.json | Yes — shared with team |
local | <repo>/.slop-mcp/memory.local/_slop/ | _slop.overrides.json, _slop.tools.json | No — git-ignored |
When the same tool has overrides in multiple scopes, the order of precedence is: local > project > user.
Overrides
An override replaces the description and/or parameter docs that search_tools and get_metadata return for an existing MCP tool. The underlying tool implementation is unchanged — only what the agent sees is different.
Setting an override
Pass set_override to customize_tools with the MCP name, tool name, and your replacement text:
{
"action": "set_override",
"mcp": "figma",
"tool": "get_file",
"description": "Fetch a Figma document by file key. Returns page tree and node metadata.",
"params": {
"file_key": "Figma file key (last path segment of the Figma URL)",
"depth": "Node traversal depth (default 1; increase for deeper component trees)"
},
"scope": "project"
}
params is a flat map of parameter name → replacement description. Omit a parameter to leave its original description intact. Omit scope to default to user.
Staleness detection
slop-mcp hashes the upstream tool's schema at the time an override is saved. If the MCP vendor later changes the tool's input schema, your override is flagged stale. To see only stale overrides:
{
"action": "list_overrides",
"stale_only": true
}
Or use the manage_mcps shortcut:
{
"action": "list_stale_overrides"
}
A stale override still applies — the agent won't see a broken schema — but you should review it to make sure your description still matches the updated parameters.
Removing an override
{
"action": "remove_override",
"mcp": "figma",
"tool": "get_file",
"scope": "project"
}
Removing an override restores the original MCP tool description at that scope. If a lower-priority scope also has an override, that one becomes active.
Custom Tools
Custom tools are new tools you define from scratch. They appear in search_tools results alongside real MCP tools and can be executed via execute_tool. Route them by passing _custom as the MCP name, or leave MCP name empty — slop-mcp checks the custom tool registry first.
Defining a custom tool
A custom tool has a name, description, JSON Schema for its inputs, and a SLOP body that runs when the tool is called. The body has access to an args map containing the validated input values.
{
"action": "define_custom",
"name": "figma_page_names",
"description": "List all page names in a Figma file.",
"inputSchema": {
"type": "object",
"properties": {
"file_key": {"type": "string", "description": "Figma file key"}
},
"required": ["file_key"]
},
"body": "doc = execute_tool(\"figma\", \"get_file\", {file_key: args[\"file_key\"]})\nmap(doc[\"pages\"], |p| p[\"name\"])",
"scope": "project"
}
The tool's return value is the last evaluated expression in the body (or any value passed to emit()).
SLOP body syntax quick reference
Custom tool bodies are SLOP scripts. Key rules:
- No
$-prefixed variables. Variables are plain identifiers:doc,pages,result. - Map field access uses bracket notation:
args["file_key"],page["name"]. - Pipe operator chains transforms:
items | filter(|x| x["active"]) | map(|x| x["name"]). - Anonymous functions use
|param|syntax:map(list, |item| item["id"]). emit()sets named output fields:emit(count: len(result), names: result).execute_tool(mcp, tool, args_map)calls any connected MCP tool.
Example body calling two MCPs and returning a merged result:
issues = execute_tool("github", "list_issues", {repo: args["repo"], state: "open"})
names = map(issues, |i| i["title"])
emit(count: len(issues), titles: names)
Arg binding and shorthand
Inside a body, all inputs arrive in the args map. For frequently accessed parameters you can bind them at the top of the body as plain variables:
file_key = args["file_key"]
depth = args["depth"]
doc = execute_tool("figma", "get_file", {file_key: file_key, depth: depth})
If a variable name you bind would shadow a SLOP built-in (e.g., map, len, filter), slop-mcp logs a warning and the binding is skipped — use args["map"] directly in that case.
Body size limit
The body string is capped at 64 KB by default. Set SLOP_MAX_CUSTOM_BODY in your environment to raise the limit. For logic that exceeds this, store the script as a recipe file and call it via run_slop with recipe: instead.
Recursion depth guard
Custom tools may call other custom tools. The call stack is limited to 16 frames (ErrCustomToolRecursion). Mutual recursion beyond this depth returns an error rather than a stack overflow.
Listing and removing custom tools
{ "action": "list_custom", "scope": "project" }
{
"action": "remove_custom",
"name": "figma_page_names",
"scope": "project"
}
Import / Export
Customizations can be packaged into a portable JSON object — a customization pack — for sharing across machines or repositories. The server never reads or writes files directly; the agent handles file I/O using its own filesystem tools.
Exporting a pack
Call export with the scope (and optionally an MCP name to filter):
{
"action": "export",
"mcp": "figma",
"scope": "project"
}
The response includes a pack field containing the full customization pack:
{
"ok": true,
"action": "export",
"affected": 4,
"pack": {
"schema_version": 1,
"scope": "project",
"overrides": [...],
"custom_tools": [...]
}
}
The agent is responsible for persisting the pack — serialize pack to a file using your filesystem tools.
Importing a pack
Read the previously saved file with your filesystem tools, then pass the JSON string as the data argument:
{
"action": "import",
"data": "<the pack JSON as a string>",
"scope": "project",
"overwrite": false
}
Importing merges the pack into the target scope. By default, existing entries are not overwritten; set overwrite: true to replace them. The schema hashes are imported as-is — if the current MCP version differs, imported overrides may immediately appear stale.
Sharing via git
- Export the pack and save it to a file in your repository (e.g.
.slop-mcp-packs/figma.json). - Commit that file to git.
- Teammates pull and then import: read the file contents and pass them as
datain animportcall.
The project scope files (.slop-mcp/memory/_slop/) are also committed, so the pack serves as an explicit snapshot that can be inspected and reviewed in pull requests.
Staleness and Upgrades
When a MCP vendor ships a schema change, overrides referencing the old schema are flagged. The hash stored at save time is compared against the current schema hash on each load. A mismatch sets the override's stale field to true.
Staleness doesn't break the tool — the override still applies and agents still see your description. But you should review the updated schema and either update the description or remove the override if the vendor's new description is acceptable.
Use list_overrides stale_only: true to find affected entries before upgrading an MCP version.
Reserved Memory Banks
The _slop.* namespace in the persistent memory store is reserved for slop-mcp internals. Custom tool bodies may read from _slop.* banks using mem_get, but writes must go through customize_tools — direct mem_save calls to _slop.* keys return an error. This prevents custom tools from corrupting the customization index or staleness hashes.
Example: Figma Compression Workflow
This walkthrough shows the full lifecycle: discovering verbose docs, trimming them, exporting, and sharing with the team.
Step 1 — Discover the problem.
The agent calls search_tools for "figma" and notices that get_file, get_node, and get_comments each return 300+ token descriptions. Context cost is significant when any figma tool is in scope.
Step 2 — Set overrides for each tool.
{
"action": "set_override",
"mcp": "figma",
"tool": "get_file",
"description": "Fetch a Figma document. Returns page tree and top-level nodes.",
"params": {
"file_key": "Figma file key (last segment of share URL)",
"depth": "Traversal depth (default 1)"
},
"scope": "project"
}
Repeat for get_node and get_comments.
Step 3 — Define a composite helper.
Rather than calling get_file and parsing the result every time, define a custom tool that returns just the page names:
{
"action": "define_custom",
"name": "figma_page_names",
"description": "Return all page names in a Figma file as a list of strings.",
"inputSchema": {
"type": "object",
"properties": {
"file_key": {"type": "string"}
},
"required": ["file_key"]
},
"body": "doc = execute_tool(\"figma\", \"get_file\", {file_key: args[\"file_key\"]})\nmap(doc[\"pages\"], |p| p[\"name\"])",
"scope": "project"
}
Step 4 — Export and commit.
{
"action": "export",
"mcp": "figma",
"scope": "project"
}
The response pack field contains the serializable pack object. Save it to .slop-mcp-packs/figma.json using your filesystem tools, then commit that file.
Step 5 — Teammates import.
After pulling, each teammate reads .slop-mcp-packs/figma.json and runs:
{
"action": "import",
"data": "<contents of figma.json as a string>",
"scope": "project"
}
From this point, every agent in the repo sees the compressed Figma tool descriptions and the figma_page_names custom tool, without any per-user setup.
Example: Caveman an Image-Generation MCP
Image-generation MCPs are some of the worst offenders for context bloat. A typical generate_image tool ships with marketing-prose descriptions, fifteen optional knobs (sampler, scheduler, cfg_scale, clip_skip, controlnet, lora_weights, ...), enums of every supported model, and parameter docs that explain Stable Diffusion's history.
Most projects use one model, one resolution, one sampler — and only ever care about the prompt. Three-stage compression turns a 420-token tool into a 12-token custom tool.
The starting point
Assume an imagegen MCP exposes a generate_image tool with this description:
Generate an image from a text prompt using a configurable diffusion pipeline. Supports SDXL, SD 1.5, SD 3.0, FLUX.1-dev, FLUX.1-schnell, and any HuggingFace-compatible checkpoint. Parameters control resolution, denoising steps, classifier-free guidance scale, sampler selection (Euler, Euler-a, DPM++ 2M, DPM++ SDE, UniPC, ...), scheduler (karras, exponential, normal), random seed for reproducibility, optional negative prompt for excluded concepts, optional LoRA weights, optional ControlNet conditioning images, optional IP-Adapter reference images, output format (png, jpeg, webp), and quality settings... [continues for 300 more tokens]
Schema has 14 parameters, 11 of them optional. Your project always uses FLUX.1-schnell at 1024×1024, 4 steps, default seed.
Stage 1 — Caveman the description
Replace the marketing description with one terse line. Replace each parameter description with what your project actually wants:
{
"action": "set_override",
"mcp": "imagegen",
"tool": "generate_image",
"description": "Generate image from prompt. Returns PNG bytes.",
"params": {
"prompt": "Image description.",
"negative_prompt": "What to avoid (optional, leave empty).",
"model": "Always pass \"flux-schnell\".",
"width": "Always pass 1024.",
"height": "Always pass 1024.",
"steps": "Always pass 4.",
"cfg_scale": "Leave unset (uses default 1.0).",
"sampler": "Leave unset.",
"scheduler": "Leave unset.",
"seed": "Leave unset for random.",
"lora_weights": "Leave unset.",
"controlnet": "Leave unset.",
"ip_adapter": "Leave unset.",
"output_format": "Always pass \"png\"."
},
"scope": "project"
}
The agent now sees a tight description and per-parameter hints that say exactly what to do. The 14-param schema is still there (overrides don't drop fields from JSON Schema — they only change descriptive text), but the prose around it shrank by ~85%.
Stage 2 — Document hardcoded values for the general endpoint
Some agents will still pass every documented parameter "to be safe." If your project genuinely never varies the optional knobs, push the message harder in the description itself:
{
"action": "set_override",
"mcp": "imagegen",
"tool": "generate_image",
"description": "Generate 1024×1024 PNG from prompt. Pass only `prompt`. Server defaults handle model (flux-schnell), steps (4), and sampler.",
"params": {
"prompt": "Image description (required).",
"negative_prompt": "DO NOT SET — project policy.",
"model": "DO NOT SET — server uses flux-schnell.",
"width": "DO NOT SET — server uses 1024.",
"height": "DO NOT SET — server uses 1024.",
"steps": "DO NOT SET — server uses 4.",
"cfg_scale": "DO NOT SET.",
"sampler": "DO NOT SET.",
"scheduler": "DO NOT SET.",
"seed": "DO NOT SET.",
"lora_weights": "DO NOT SET.",
"controlnet": "DO NOT SET.",
"ip_adapter": "DO NOT SET.",
"output_format": "DO NOT SET — server returns PNG."
},
"scope": "project"
}
This step is about agent compliance, not schema compression. Either rely on MCP-server-side defaults to fire when the field is absent, or accept that some agents will dutifully echo your policy back as a confirmation step (still cheaper than ten probabilistic samplings of "should I set cfg_scale?").
Stage 3 — Wrap with a custom SLOP tool
For the truly minimal interface, define a brand-new tool with a one-field schema. The body calls the underlying MCP with all the hardcoded boilerplate — the agent never sees the 14 parameters at all.
{
"action": "define_custom",
"name": "thumbnail",
"description": "Generate 1024×1024 thumbnail PNG from a prompt.",
"inputSchema": {
"type": "object",
"properties": {
"prompt": {"type": "string", "description": "Image description."}
},
"required": ["prompt"]
},
"body": "execute_tool(\"imagegen\", \"generate_image\", {prompt: args[\"prompt\"], model: \"flux-schnell\", width: 1024, height: 1024, steps: 4, output_format: \"png\"})",
"scope": "project"
}
What search_tools returns now:
thumbnail
Generate 1024×1024 thumbnail PNG from a prompt.
Input: {prompt: string}
Twelve tokens of description, one parameter, no enums, no optional knobs. The original generate_image is still callable for the rare case you need a different model or resolution — the custom tool is purely additive.
Tiered exposure pattern
You don't have to commit to one stage. Stack them: keep generate_image available with a Stage-2 override for advanced cases, and define thumbnail as the default path. Agents pick the cheaper tool by default and fall back to the verbose one only when the simple wrapper isn't enough.
search_tools("image")
→ thumbnail (cheap, one param, 12 tokens)
→ product_hero_image (1920×1080 wrapper, 18 tokens)
→ social_card (1200×630 wrapper, 18 tokens)
→ generate_image (Stage-2 override, full control if needed)
A custom-tool zoo plus a hardened override is often the right answer: 90% of calls hit a tiny custom tool, the long tail still has the full MCP.
Bundling and sharing
Once the overrides + custom tools are validated, export them as a pack:
{ "action": "export", "mcp": "imagegen", "scope": "project" }
The response includes any custom tools that reference the imagegen MCP via execute_tool calls in their bodies, so thumbnail ships with the pack. Save the pack to .slop-mcp-packs/imagegen.json, commit it, and the rest of the team gets the same compressed image-generation interface on git pull + import.