SLOPSHOPPER

mcp-diet

Choose which MCP tools and subagent types Claude sees, per project. A pane lists every server and tool with the size of its description; defer or hide the ones…

newpanecommandagents
v0.1.0MITupdated 2026-10-09MDmubarak786/claude-mods/mods/mcp-diet
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mcp-diet
│ ┃ MCP diet ✕ › fix the failing auth test and add an audit log call │ ┃ MCP tools and subagent types for this │ ┃ project. Changes apply next session. ⏺ Read(src/auth.ts) │ ┃ No MCP tools or subagent types seen in this ⎿ Read 6 lines │ ┃ session. ⏺ Update(src/auth.ts) │ ┃ Press a button to cycle on → defer → hide. ⎿ Added 2 lines, removed 1 line │ ┃ Esc closes. ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /mcp-diet │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · MCP diet
MCP tools and subagent types for this project. Changes apply next session. No MCP tools or subagent types seen in this session. Press a button to cycle on → defer → hide. Esc closes.
README

mcp-diet

Choose which MCP tools and subagent types Claude sees, per project. Every connected MCP server sends its tool descriptions with every request; four servers have cost people tens of thousands of tokens before the first prompt. Claude Code's tool search helps across the board, but you can't say "in this repo, Claude never needs the Jira tools." Now you can.

A pane lists every server and tool with the size of its description. Press a button to cycle a tool, a whole server, or a subagent type through on, defer (Claude sees the name only, until it searches for it), and hide (withheld). Choices are saved per project.

Install

/plugin marketplace add MDmubarak786/claude-mods
/plugin install mcp-diet@modhub

Try it for one session without installing:

claude --plugin-dir ./mods/mcp-diet

Use it

CommandWhat it does
/mcp-dietOpen the pane. Where no pane can be drawn, print the list instead.
/mcp-diet listPrint every server, tool, and subagent type seen this session, with token estimates and current choices.
/mcp-diet defer <name>Claude sees the tool's name only, until it searches for it.
/mcp-diet hide <name>The tool or subagent type is withheld from Claude.
/mcp-diet show <name>Back to normal.

<name> is a tool as Claude sees it (mcp__jira__create_issue), a whole server (mcp__jira), or a subagent type (pr-review-toolkit:code-reviewer). A tool's own choice wins over its server's.

When it applies. Claude Code asks a mod for a tool's description once, when the tool is first sent to Claude. A change you make therefore applies to the next session, not the running one. The pane and the commands say so.

What it touches

From claude plugin validate ./mods/mcp-diet:

hooks: session.start, tool.describe, agent.offer, command.run{command=mcp-diet}, ui.render{component=Pane}
calls: $.command.register, $.session.root (via loadChoices), $.store.get (via loadChoices), $.store.set (via saveChoices), $.tool.list (via inventory), $.ui.invalidate, $.ui.log, $.ui.open, $.ui.resolve
  • tool.describe returns a stub description and isDeferred: true for a hidden tool, and isDeferred: true with the real description for a deferred one. Nothing else about a tool changes.
  • agent.offer returns isOffered: false for a hidden subagent type.
  • $.store holds the choices, keyed by project root. No files, processes, or network.

Tested with

  • Claude Code 2.1.295, claude plugin validate --strict and claude plugin test pass, including a mounted pane test. /mcp-diet list answered from a live claude -p session. The pane hasn't been seen on screen by the author yet.

Limitations

  • Takes effect next session, for the reason above.
  • Token estimates are description length divided by four. The real cost also includes each tool's input schema, which the mods API doesn't expose.
  • Subagent types appear in the list only after Claude Code has offered them in the session, which happens at start.
  • Hiding a tool doesn't disconnect its server. The server still starts; its description just isn't sent.

License

MIT, see the repository root.

Source 1 files
hooks/register.ts 154 lines
1// mcp-diet: choose which MCP tools and subagent types Claude sees, per project.
2//
3//   /mcp-diet                     open the pane (or print the list where nothing draws)
4//   /mcp-diet list                print every server, tool, and agent type with sizes
5//   /mcp-diet defer <name>        Claude sees the tool's name only, until it searches for it
6//   /mcp-diet hide <name>         the tool or agent type is withheld from Claude
7//   /mcp-diet show <name>         back to normal
8//
9// <name> is a tool (mcp__server__tool), a whole server (mcp__server), or a
10// subagent type. Choices are saved per project. Claude Code asks for a tool's
11// description once, when it's first sent, so a change applies to the next
12// session, not the running one.
13
14type Mode = 'defer' | 'hide'
15type Choices = Record<string, Mode>
16
17let root = ''
18let choices: Choices = {}
19const agentTypes = new Map<string, string>() // name -> description, as offered this session
20
21const PANE = 'mcp-diet'
22
23async function loadChoices($) {
24  try {
25    root = await $.session.root()
26    const saved = await $.store.get('mcp-diet:' + root)
27    choices = saved && typeof saved === 'object' ? (saved as Choices) : {}
28  } catch {
29    choices = {}
30  }
31}
32
33async function saveChoices($) {
34  await $.store.set('mcp-diet:' + root, choices)
35}
36
37function serverOf(tool: string): string | null {
38  const m = /^(mcp__[^_]+(?:_[^_]+)*?)__/.exec(tool)
39  return m ? m[1] : null
40}
41
42// The most specific choice wins: tool, then its server.
43function modeFor(name: string): Mode | null {
44  if (choices[name]) return choices[name]
45  const server = serverOf(name)
46  if (server && choices[server]) return choices[server]
47  return null
48}
49
50function tokens(chars: number): number {
51  return Math.round(chars / 4)
52}
53
54async function inventory($) {
55  const tools = await $.tool.list()
56  const servers = new Map<string, { name: string; description: string; mode: Mode | null }[]>()
57  for (const t of tools) {
58    if (!t.mcp) continue
59    const server = serverOf(t.name) ?? 'mcp'
60    if (!servers.has(server)) servers.set(server, [])
61    servers.get(server)!.push({ name: t.name, description: t.description ?? '', mode: modeFor(t.name) })
62  }
63  return servers
64}
65
66function listText(servers: Map<string, { name: string; description: string; mode: Mode | null }[]>): string {
67  const lines: string[] = []
68  let total = 0
69  for (const [server, tools] of servers) {
70    const chars = tools.reduce((n, t) => n + t.description.length, 0)
71    total += tools.filter((t) => !t.mode).reduce((n, t) => n + t.description.length, 0)
72    lines.push(server + (choices[server] ? ' [' + choices[server] + ']' : '') + ': ' + tools.length + ' tool(s), ~' + tokens(chars) + ' tokens of descriptions')
73    for (const t of tools) lines.push('  ' + t.name.slice(server.length + 2) + (t.mode ? ' [' + t.mode + ']' : '') + ' ~' + tokens(t.description.length))
74  }
75  for (const [name, description] of agentTypes) lines.push('agent ' + name + (choices[name] ? ' [' + choices[name] + ']' : '') + ' ~' + tokens(description.length))
76  if (!lines.length) return 'No MCP tools or subagent types seen in this session.'
77  lines.push('About ' + tokens(total) + ' tokens of MCP tool descriptions are sent in full. Changes apply to the next session.')
78  return lines.join('\n')
79}
80
81export function register(on) {
82  on('session.start', async ($, e, next) => {
83    await loadChoices($)
84    try {
85      await $.command.register({ name: 'mcp-diet', description: 'Defer or hide MCP tools and subagent types for this project', argumentHint: '[list | defer|hide|show <name>]' })
86    } catch (error) {
87      $.ui.log('could not register /mcp-diet: ' + error)
88    }
89    return next(e)
90  })
91
92  // Claude Code asks for each tool's description once; answer from the saved choice.
93  on('tool.describe', async ($, e, next) => {
94    const mode = modeFor(e.tool)
95    if (!mode) return next(e)
96    if (mode === 'hide') return { description: 'Not available in this project.', isDeferred: true }
97    const theirs = await next(e)
98    return { ...theirs, isDeferred: true }
99  })
100
101  on('agent.offer', async ($, e, next) => {
102    agentTypes.set(e.agent, e.description ?? '')
103    if (choices[e.agent] === 'hide') return { isOffered: false }
104    return next(e)
105  }).catch(async ($, e, next) => next(e))
106
107  on('command.run', { command: 'mcp-diet' }, async ($, e) => {
108    const args = e.args.trim()
109    const m = /^(defer|hide|show)\s+(\S+)$/.exec(args)
110    if (m) {
111      const [, verb, name] = m
112      if (verb === 'show') delete choices[name]
113      else choices[name] = verb as Mode
114      await saveChoices($)
115      $.ui.invalidate('ui.render')
116      return { text: (verb === 'show' ? 'Showing ' : verb === 'defer' ? 'Deferring ' : 'Hiding ') + name + ' in this project from the next session on.' }
117    }
118    const servers = await inventory($)
119    if (args === 'list') return { text: listText(servers) }
120    if (args) return { text: 'Usage: /mcp-diet, /mcp-diet list, or /mcp-diet defer|hide|show <name>' }
121    const placed = await $.ui.open({ id: PANE, title: 'MCP diet', focus: true, closeOnEscape: true })
122    if (!placed.isPlaced) return { text: listText(servers) }
123    return {}
124  }).catch(async () => ({ text: 'mcp-diet: the command failed, so nothing changed.' }))
125
126  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
127    if (e.requestId !== PANE) return next(e)
128    const { Box, Text, Button } = $.ui.resolve(e)
129    const servers = await inventory($)
130    const rows: unknown[] = [Text({ bold: true, children: ['MCP tools and subagent types for this project. Changes apply next session.'] })]
131    const cycle = (name: string) => async () => {
132      const current = choices[name]
133      if (!current) choices[name] = 'defer'
134      else if (current === 'defer') choices[name] = 'hide'
135      else delete choices[name]
136      await saveChoices($)
137      $.ui.invalidate('ui.render')
138    }
139    const label = (name: string) => (choices[name] ?? 'on')
140    for (const [server, tools] of servers) {
141      rows.push(Box({ flexDirection: 'row', columnGap: 1, children: [Button({ key: 'srv-' + server, label: label(server), plain: true, onPress: cycle(server) }), Text({ bold: true, children: [server + '  ~' + tokens(tools.reduce((n, t) => n + t.description.length, 0)) + ' tokens'] })] }))
142      for (const t of tools) {
143        rows.push(Box({ flexDirection: 'row', columnGap: 1, paddingLeft: 2, children: [Button({ key: 'tool-' + t.name, label: label(t.name), plain: true, onPress: cycle(t.name) }), Text({ dimColor: !!t.mode, children: [t.name.slice(server.length + 2) + '  ~' + tokens(t.description.length)] })] }))
144      }
145    }
146    for (const [name, description] of agentTypes) {
147      rows.push(Box({ flexDirection: 'row', columnGap: 1, children: [Button({ key: 'agent-' + name, label: choices[name] === 'hide' ? 'hide' : 'on', plain: true, onPress: async () => { if (choices[name] === 'hide') delete choices[name]; else choices[name] = 'hide'; await saveChoices($); $.ui.invalidate('ui.render') } }), Text({ children: ['agent ' + name + '  ~' + tokens(description.length)] })] }))
148    }
149    if (rows.length === 1) rows.push(Text({ dimColor: true, children: ['No MCP tools or subagent types seen in this session.'] }))
150    rows.push(Text({ dimColor: true, children: ['Press a button to cycle on → defer → hide. Esc closes.'] }))
151    return Box({ flexDirection: 'column', children: rows })
152  })
153}
154