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…

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.
/plugin marketplace add MDmubarak786/claude-mods
/plugin install mcp-diet@modhub
Try it for one session without installing:
claude --plugin-dir ./mods/mcp-diet
| Command | What it does |
|---|---|
/mcp-diet | Open the pane. Where no pane can be drawn, print the list instead. |
/mcp-diet list | Print 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.
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.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.MIT, see the repository root.
hooks/register.ts 154 lines1// 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