Connector control center: how many tokens each MCP connector eats (schemas and replies), its version and what to disable, in the /connectors pane

A Claude Code mod that shows how many tokens each MCP connector costs you — and what to switch off.
Every connected MCP server puts its tool schemas into the context window on every turn, and every tool call brings back a reply that stays in the conversation. With ten or twenty connectors that adds up fast, and Claude Code doesn't show you where it goes. /connectors does.
npx server runs, the installed version, and whether a newer one exists on npm.limit/fields, or filter in a script);npx …@latest servers;Click a connector (or run /connectors <name>) to see its tools ranked by schema size.
Token figures are estimates (≈4 Latin or ≈2 Cyrillic characters per token), shown with ≈ everywhere.
In a Claude Code terminal session:
/plugin install connector-center --marketplace narkov/claude-code-connector-center
Answer y to add the marketplace, then pick a scope (user scope loads it in every session).
/connectors # open the pane: all connectors, ranked by context cost
/connectors ga4 # jump to a connector by name
In the pane: r refresh, u check npm for updates, 0 back to the full list. A status-line entry shows the total MCP schema cost in context.
claude plugin validate .
claude plugin test .
claude --plugin-dir .
MIT
hooks/register.tsx 605 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type {
5 ConnectorCalls,
6 ConnectorHistory,
7 ConnectorKind,
8 ConnectorServer,
9 ConnectorTool,
10} from '../types'
11
12const PANE = 'connector-center'
13const TITLE = 'Connectors'
14const STORE_KEY = 'history'
15const DAY = 24 * 60 * 60 * 1000
16const WIDE = 84
17const IDLE_TOKENS = 1500
18const HEAVY_REPLY = 6000
19
20const servers = atom({ plugin: 'connector-center', key: 'servers' } as const, [])
21const context = atom({ plugin: 'connector-center', key: 'context' } as const, null)
22const calls = atom({ plugin: 'connector-center', key: 'calls' } as const, {})
23const history = atom({ plugin: 'connector-center', key: 'history' } as const, {})
24const latest = atom({ plugin: 'connector-center', key: 'latest' } as const, {})
25const checking = atom({ plugin: 'connector-center', key: 'checking' } as const, false)
26const selected = atom({ plugin: 'connector-center', key: 'selected' } as const, null)
27
28type Json = Record<string, unknown>
29type Config = Omit<ConnectorServer, 'tools'>
30
31const obj = (value: unknown): Json =>
32 value !== null && typeof value === 'object' && !Array.isArray(value) ? (value as Json) : {}
33const strings = (value: unknown): string[] =>
34 Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []
35
36// Byte-pair tokenizers spend about a token per 4 Latin characters and per 2
37// Cyrillic ones: a rough figure, shown with ≈ everywhere.
38const estimate = (text: string): number => {
39 let ascii = 0
40 let other = 0
41 for (const ch of text) {
42 if (ch.charCodeAt(0) < 128) ascii += 1
43 else other += 1
44 }
45
46 return Math.ceil(ascii / 4 + other / 2)
47}
48
49const fmt = (n: number): string =>
50 n >= 10_000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
51
52const newer = (a: string, b: string): boolean => {
53 const pa = a.split(/[.-]/).map(Number)
54 const pb = b.split(/[.-]/).map(Number)
55 for (let i = 0; i < 3; i += 1) {
56 const x = pa[i] ?? 0
57 const y = pb[i] ?? 0
58 if (x !== y) return x > y
59 }
60
61 return false
62}
63
64const readJson = async ($: EngineInterface, path: string): Promise<Json | null> => {
65 try {
66 return obj(JSON.parse(await $.fs.read(path)))
67 } catch {
68 return null
69 }
70}
71
72const splitSpec = (arg: string): { pkg: string; spec: string | null } => {
73 const at = arg.indexOf('@', arg.startsWith('@') ? 1 : 0)
74
75 return at > 0 ? { pkg: arg.slice(0, at), spec: arg.slice(at + 1) } : { pkg: arg, spec: null }
76}
77
78const dirOf = (path: string): string => path.slice(0, Math.max(0, path.lastIndexOf('/')))
79
80const packageAbove = async (
81 $: EngineInterface,
82 file: string,
83): Promise<{ name: string | null; version: string | null }> => {
84 let dir = dirOf(file)
85 for (let up = 0; up < 4 && dir !== ''; up += 1) {
86 const found = await readJson($, `${dir}/package.json`)
87 if (found !== null) {
88 return {
89 name: typeof found.name === 'string' ? found.name : null,
90 version: typeof found.version === 'string' ? found.version : null,
91 }
92 }
93 dir = dirOf(dir)
94 }
95
96 return { name: null, version: null }
97}
98
99const describe = async (
100 $: EngineInterface,
101 name: string,
102 scope: string,
103 entry: Json,
104 root: string,
105): Promise<Config> => {
106 const base: Config = { name, kind: 'unknown', scope, pkg: null, spec: null, installed: null }
107 const command = typeof entry.command === 'string' ? entry.command : ''
108 const args = strings(entry.args).filter(arg => !arg.startsWith('-'))
109 if (typeof entry.url === 'string' || entry.type === 'http' || entry.type === 'sse') {
110 return { ...base, kind: 'remote' }
111 }
112 if (command.endsWith('npx') && args[0] !== undefined) {
113 const { pkg, spec } = splitSpec(args[0])
114
115 return { ...base, kind: pkg === 'mcp-remote' ? 'remote' : 'npx', pkg, spec }
116 }
117 if (command.endsWith('node')) {
118 const script = args.find(arg => /\.(c|m)?js$/.test(arg))
119 if (script !== undefined) {
120 const path = script.startsWith('/') ? script : `${root}/${script}`
121 const found = await packageAbove($, path)
122
123 return { ...base, kind: 'node', pkg: found.name, installed: found.version }
124 }
125 }
126
127 return { ...base, kind: command === '' ? 'unknown' : 'command' }
128}
129
130const readConfigs = async ($: EngineInterface): Promise<Config[]> => {
131 const root = await $.session.root()
132 const home = (await $.env.get('HOME')) ?? ''
133 const project = obj((await readJson($, `${root}/.mcp.json`))?.mcpServers)
134 const global = await readJson($, `${home}/.claude.json`)
135 const user = obj(global?.mcpServers)
136 const local = obj(obj(obj(global?.projects)[root]).mcpServers)
137
138 const found: Config[] = []
139 for (const [scope, list] of [
140 ['local', local],
141 ['.mcp.json', project],
142 ['user', user],
143 ] as const) {
144 for (const [name, entry] of Object.entries(list)) {
145 if (found.some(c => c.name === name)) continue
146 found.push(await describe($, name, scope, obj(entry), root))
147 }
148 }
149
150 // npx keeps each package it ran under ~/.npm/_npx/<hash>/node_modules.
151 const wanted = [...new Set(found.filter(c => c.kind !== 'node' && c.pkg).map(c => c.pkg as string))]
152 const installed: Record<string, string> = {}
153 let caches: string[] = []
154 try {
155 caches = (await $.fs.list(`${home}/.npm/_npx`)).filter(e => e.kind === 'dir').map(e => e.name)
156 } catch {
157 caches = []
158 }
159 for (const cache of caches) {
160 for (const pkg of wanted) {
161 const meta = await readJson($, `${home}/.npm/_npx/${cache}/node_modules/${pkg}/package.json`)
162 const version = meta?.version
163 if (typeof version !== 'string') continue
164 const known = installed[pkg]
165 if (known === undefined || newer(version, known)) installed[pkg] = version
166 }
167 }
168
169 return found.map(c =>
170 c.kind !== 'node' && c.pkg !== null ? { ...c, installed: installed[c.pkg] ?? null } : c,
171 )
172}
173
174// The configs change rarely and their scan reads the npx cache: done at
175// start and on an explicit refresh, while the token figures follow each turn.
176let configs: Config[] | null = null
177
178const refresh = async ($: EngineInterface, rescan = false): Promise<void> => {
179 if (configs === null || rescan) {
180 try {
181 configs = await readConfigs($)
182 } catch {
183 configs = []
184 }
185 }
186 const usage = await $.session.usage({ breakdown: 'summary' })
187 const breakdown = usage.context.breakdown
188 const byServer = new Map<string, ConnectorTool[]>()
189 for (const tool of breakdown?.mcpTools ?? []) {
190 const list = byServer.get(tool.serverName) ?? []
191 list.push({ name: tool.name, tokens: tool.tokens, isLoaded: tool.isLoaded })
192 byServer.set(tool.serverName, list)
193 }
194
195 const list: ConnectorServer[] = configs.map(c => ({ ...c, tools: byServer.get(c.name) ?? [] }))
196 for (const [name, tools] of byServer) {
197 if (list.some(s => s.name === name)) continue
198 const kind: ConnectorKind = /^claude[._ ]ai[ _]/.test(name)
199 ? 'claude.ai'
200 : name.startsWith('plugin:')
201 ? 'plugin'
202 : 'unknown'
203 list.push({ name, kind, scope: kind, pkg: null, spec: null, installed: null, tools })
204 }
205
206 const mcpLoaded = list.reduce((sum, s) => sum + loadedOf(s), 0)
207 await update($, servers, () => list)
208 await update($, context, () => ({
209 used: breakdown?.totalTokens ?? usage.context.tokens ?? null,
210 window: usage.context.window,
211 mcpLoaded,
212 }))
213 const connected = list.filter(s => s.tools.length > 0).length
214 $.ui.status(`MCP ≈${fmt(mcpLoaded)} in context · ${connected} connectors`)
215}
216
217const loadedOf = (s: ConnectorServer): number =>
218 s.tools.reduce((sum, t) => sum + (t.isLoaded ? t.tokens : 0), 0)
219const deferredOf = (s: ConnectorServer): number =>
220 s.tools.reduce((sum, t) => sum + (t.isLoaded ? 0 : t.tokens), 0)
221
222const callsOf = (s: ConnectorServer, all: Record<string, ConnectorCalls>): ConnectorCalls => {
223 const total: ConnectorCalls = { calls: 0, errors: 0, tokens: 0, max: 0 }
224 const prefix = `mcp__${s.name.replace(/[^A-Za-z0-9_-]/g, '_')}__`
225 for (const [tool, c] of Object.entries(all)) {
226 if (!s.tools.some(t => t.name === tool) && !tool.startsWith(prefix)) continue
227 total.calls += c.calls
228 total.errors += c.errors
229 total.tokens += c.tokens
230 total.max = Math.max(total.max, c.max)
231 }
232
233 return total
234}
235
236const serverOfTool = (tool: string, list: ConnectorServer[]): string => {
237 const owner = list.find(s => s.tools.some(t => t.name === tool))
238 if (owner !== undefined) return owner.name
239 const match = /^mcp__(.+?)__/.exec(tool)
240
241 return match?.[1] ?? tool
242}
243
244const checkUpdates = async ($: EngineInterface): Promise<void> => {
245 await update($, checking, () => true)
246 const list = await read($, servers)
247 const pkgs = [...new Set(list.filter(s => s.kind !== 'node' && s.pkg).map(s => s.pkg as string))]
248 for (const pkg of pkgs) {
249 try {
250 const run = await $.process.run(['npm', 'view', pkg, 'version'], { timeoutMs: 20_000 })
251 const version = run.stdout.trim().split('\n').pop()?.trim()
252 if (run.exitCode === 0 && version) await update($, latest, all => ({ ...all, [pkg]: version }))
253 } catch {
254 // npm missing or offline: leave that package unchecked.
255 }
256 }
257 await update($, checking, () => false)
258}
259
260type Hint = { text: string; color?: string }
261
262const hintsFor = (
263 list: ConnectorServer[],
264 all: Record<string, ConnectorCalls>,
265 past: Record<string, ConnectorHistory>,
266 newest: Record<string, string>,
267 now: number,
268): Hint[] => {
269 const hints: Hint[] = []
270
271 const copies = new Map<string, ConnectorServer[]>()
272 for (const s of list) {
273 if (s.kind !== 'npx' || s.pkg === null) continue
274 copies.set(s.pkg, [...(copies.get(s.pkg) ?? []), s])
275 }
276 for (const [pkg, same] of copies) {
277 if (same.length < 2) continue
278 const schemas = same.reduce((sum, s) => sum + loadedOf(s) + deferredOf(s), 0)
279 const used = same.filter(s => callsOf(s, all).calls > 0 || (past[s.name]?.calls ?? 0) > 0)
280 hints.push({
281 text: `${same.length}× ${pkg}: ≈${fmt(schemas)} tokens of identical schemas, ${used.length} of them called. Keep only the ones this project needs, or run one server that takes the property as an argument.`,
282 color: 'yellow',
283 })
284 }
285
286 for (const s of list) {
287 const here = callsOf(s, all)
288 const before = past[s.name]
289 const loaded = loadedOf(s)
290 const idle = here.calls === 0 && (before === undefined || now - before.lastAt > 14 * DAY)
291 if (loaded >= IDLE_TOKENS && idle) {
292 hints.push({
293 text: `${s.name}: ≈${fmt(loaded)} in context every turn, not called ${before ? 'for over 14 days' : 'even once'}. Disable it in /mcp or move it to the project that needs it.`,
294 color: 'red',
295 })
296 }
297 if (here.calls >= 2 && here.tokens / here.calls >= HEAVY_REPLY) {
298 hints.push({
299 text: `${s.name}: average reply ≈${fmt(Math.round(here.tokens / here.calls))}, largest ≈${fmt(here.max)}. Ask for limit/fields or filter in a script.`,
300 color: 'yellow',
301 })
302 }
303 if (s.kind === 'npx' && (s.spec === null || s.spec === 'latest')) {
304 hints.push({
305 text: `${s.name}: npx ${s.pkg}${s.spec ? '@latest' : ''} is not pinned, the version can change between starts.`,
306 })
307 }
308 const fresh = s.pkg ? newest[s.pkg] : undefined
309 if (fresh !== undefined && s.installed !== null && newer(fresh, s.installed)) {
310 hints.push({ text: `${s.name}: update available ${s.installed} → ${fresh}.`, color: 'cyan' })
311 }
312 if (s.tools.length === 0 && (s.scope === '.mcp.json' || s.scope === 'user' || s.scope === 'local')) {
313 hints.push({ text: `${s.name}: configured but has no tools (failed to connect or disabled).` })
314 }
315 }
316
317 return hints
318}
319
320const versionOf = (s: ConnectorServer, newest: Record<string, string>): string => {
321 if (s.kind === 'claude.ai') return 'claude.ai'
322 if (s.kind === 'remote' && s.pkg === null) return 'remote'
323 const fresh = s.pkg ? newest[s.pkg] : undefined
324 const have = s.installed ?? (s.spec && s.spec !== 'latest' ? s.spec : null)
325 if (have === null) return fresh ? `? → ${fresh}` : '—'
326
327 return fresh !== undefined && newer(fresh, have) ? `${have} → ${fresh}` : have
328}
329
330export const register: Register = on => {
331 on('session.start', async ($, e, next) => {
332 await $.command.register({
333 name: 'connectors',
334 description: "Connector control center: tokens per MCP connector, versions, what to disable",
335 })
336 try {
337 const saved = obj(await $.store.get(STORE_KEY)) as Record<string, ConnectorHistory>
338 await update($, history, () => saved)
339 await refresh($)
340 } catch {
341 $.ui.status(undefined)
342 }
343
344 return next(e)
345 })
346
347 on('turn.complete', async ($, e, next) => {
348 const done = await next(e)
349 if (e.agentId === undefined) {
350 try {
351 await refresh($)
352 } catch {
353 // The figures stay as they were until the next turn.
354 }
355 }
356
357 return done
358 })
359
360 on('tool.call', async ($, e, next) => {
361 const ran = await next(e)
362 const tool = String(e.tool)
363 if (!tool.startsWith('mcp__') || 'deny' in ran) return ran
364 try {
365 const tokens = estimate(ran.text ?? '')
366 const isError = ran.isError === true
367 await update($, calls, all => {
368 const was = all[tool] ?? { calls: 0, errors: 0, tokens: 0, max: 0 }
369
370 return {
371 ...all,
372 [tool]: {
373 calls: was.calls + 1,
374 errors: was.errors + (isError ? 1 : 0),
375 tokens: was.tokens + tokens,
376 max: Math.max(was.max, tokens),
377 },
378 }
379 })
380 const server = serverOfTool(tool, await read($, servers))
381 const now = await $.clock.now()
382 const past = await update($, history, all => {
383 const was = all[server] ?? { calls: 0, tokens: 0, lastAt: 0 }
384
385 return { ...all, [server]: { calls: was.calls + 1, tokens: was.tokens + tokens, lastAt: now } }
386 })
387 await $.store.set(STORE_KEY, past)
388 } catch {
389 // Bookkeeping never costs the call its result.
390 }
391
392 return ran
393 })
394
395 on('command.run', { command: 'connectors' }, async ($, e) => {
396 await refresh($, true)
397 const list = await read($, servers)
398 const all = await read($, calls)
399 const ctx = await read($, context)
400 const want = e.args.trim().toLowerCase()
401 const match = want === '' ? undefined : list.find(s => s.name.toLowerCase().includes(want))
402 await update($, selected, () => match?.name ?? null)
403 await $.ui.open({ id: PANE, title: TITLE, columns: 110, rows: 34, focus: true })
404
405 const ranked = [...list]
406 .sort((a, b) => loadedOf(b) - loadedOf(a) || deferredOf(b) - deferredOf(a))
407 .slice(0, 8)
408
409 return {
410 text: [
411 `Connectors: MCP schemas ≈${fmt(ctx?.mcpLoaded ?? 0)} in context, ${list.length} connectors.`,
412 ...ranked.map(s => {
413 const c = callsOf(s, all)
414
415 return `${s.name}: ${s.tools.length} tools, ≈${fmt(loadedOf(s))} in context, ≈${fmt(deferredOf(s))} deferred, ${c.calls} calls ≈${fmt(c.tokens)}, version ${s.installed ?? '—'}`
416 }),
417 ].join('\n'),
418 }
419 })
420
421 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
422 const { Box, Text, Button } = $.ui.resolve(e)
423 const list = await read($, servers)
424 const all = await read($, calls)
425 const past = await read($, history)
426 const newest = await read($, latest)
427 const busy = await read($, checking)
428 const pick = await read($, selected)
429 const ctx = await read($, context)
430 const columns = e.props.bodyColumns
431 const rows = e.viewport?.rows ?? 24
432 const now = await $.clock.now()
433
434 const controls = (
435 <Box flexDirection="row" gap={2}>
436 {pick !== null && (
437 <Button key="all" hotkey="0" plain onPress={() => void update($, selected, () => null)}>
438 All
439 </Button>
440 )}
441 <Button key="refresh" hotkey="r" plain onPress={() => void refresh($, true)}>
442 Refresh
443 </Button>
444 <Button key="updates" hotkey="u" plain onPress={() => void checkUpdates($)}>
445 {busy ? 'Checking npm…' : 'Check for updates'}
446 </Button>
447 </Box>
448 )
449
450 const header = ctx && (
451 <Text dimColor wrap="truncate-end">
452 Context {ctx.used === null ? '—' : `≈${fmt(ctx.used)}`} / {fmt(ctx.window)} · MCP schemas ≈
453 {fmt(ctx.mcpLoaded)} in context · ≈ estimate, replies are this session
454 </Text>
455 )
456
457 const server = list.find(s => s.name === pick)
458 if (server !== undefined) {
459 const total = callsOf(server, all)
460 const before = past[server.name]
461 const tools = [...server.tools].sort((a, b) => b.tokens - a.tokens)
462 const room = Math.max(1, rows - 12)
463
464 return (
465 <Box flexDirection="column">
466 {controls}
467 <Text bold>{server.name}</Text>
468 <Text dimColor wrap="truncate-end">
469 {server.kind} · {server.scope}
470 {server.pkg ? ` · ${server.pkg}${server.spec ? `@${server.spec}` : ''}` : ''} · version{' '}
471 {versionOf(server, newest)}
472 </Text>
473 <Text>
474 {tools.length} tools · ≈{fmt(loadedOf(server))} in context · ≈{fmt(deferredOf(server))}{' '}
475 deferred
476 </Text>
477 <Text>
478 Session: {total.calls} calls · ≈{fmt(total.tokens)} in replies · max ≈{fmt(total.max)}
479 {total.errors > 0 ? ` · errors ${total.errors}` : ''}
480 </Text>
481 <Text dimColor>
482 All time: {before ? `${before.calls} calls, ≈${fmt(before.tokens)}, last ${Math.round((now - before.lastAt) / DAY)} d ago` : 'no calls yet'}
483 </Text>
484 <Text> </Text>
485 {tools.slice(0, room).map(tool => {
486 const c = all[tool.name]
487
488 return (
489 <Box flexDirection="row">
490 <Box width={8}>
491 <Text color={tool.isLoaded ? 'yellow' : undefined} dimColor={!tool.isLoaded}>
492 {tool.isLoaded ? '●' : '○'} {fmt(tool.tokens)}
493 </Text>
494 </Box>
495 <Text wrap="truncate-end">
496 {tool.name.replace(/^mcp__.+?__/, '')}
497 {c ? ` ${c.calls}× ≈${fmt(c.tokens)}` : ''}
498 </Text>
499 </Box>
500 )
501 })}
502 {tools.length > room && <Text dimColor>… {tools.length - room} more</Text>}
503 <Text dimColor>● in context every turn · ○ deferred, loaded via ToolSearch</Text>
504 </Box>
505 )
506 }
507
508 const ranked = [...list].sort(
509 (a, b) =>
510 loadedOf(b) + callsOf(b, all).tokens - (loadedOf(a) + callsOf(a, all).tokens) ||
511 deferredOf(b) - deferredOf(a),
512 )
513 const hints = hintsFor(list, all, past, newest, now)
514 const hintRoom = Math.max(2, Math.floor((rows - 8) / 3))
515 const tableRoom = Math.max(3, rows - 8 - Math.min(hints.length, hintRoom))
516
517 const nameButton = (s: ConnectorServer, index: number) => (
518 <Button
519 key={`server:${s.name}`}
520 label={s.name}
521 hotkey={index < 9 ? String(index + 1) : undefined}
522 plain
523 dimColor={s.tools.length === 0}
524 onPress={() => void update($, selected, () => s.name)}
525 />
526 )
527
528 return (
529 <Box flexDirection="column">
530 {controls}
531 {header}
532 <Text> </Text>
533 {columns >= WIDE && (
534 <Box flexDirection="row">
535 <Box width={26}>
536 <Text dimColor>Connector</Text>
537 </Box>
538 <Box width={7}>
539 <Text dimColor>tools</Text>
540 </Box>
541 <Box width={11}>
542 <Text dimColor>context</Text>
543 </Box>
544 <Box width={10}>
545 <Text dimColor>deferred</Text>
546 </Box>
547 <Box width={14}>
548 <Text dimColor>calls</Text>
549 </Box>
550 <Text dimColor>version</Text>
551 </Box>
552 )}
553 {ranked.slice(0, tableRoom).map((s, index) => {
554 const c = callsOf(s, all)
555 const loaded = loadedOf(s)
556
557 return columns >= WIDE ? (
558 <Box flexDirection="row">
559 <Box width={26}>{nameButton(s, index)}</Box>
560 <Box width={7}>
561 <Text>{s.tools.length}</Text>
562 </Box>
563 <Box width={11}>
564 <Text color={loaded >= IDLE_TOKENS ? 'yellow' : undefined} dimColor={loaded === 0}>
565 ≈{fmt(loaded)}
566 </Text>
567 </Box>
568 <Box width={10}>
569 <Text dimColor>≈{fmt(deferredOf(s))}</Text>
570 </Box>
571 <Box width={14}>
572 <Text dimColor={c.calls === 0}>
573 {c.calls}× ≈{fmt(c.tokens)}
574 </Text>
575 </Box>
576 <Text dimColor wrap="truncate-end">
577 {versionOf(s, newest)}
578 </Text>
579 </Box>
580 ) : (
581 <Box flexDirection="column">
582 {nameButton(s, index)}
583 <Text dimColor wrap="truncate-end">
584 {' '}≈{fmt(loaded)} ctx · ≈{fmt(deferredOf(s))} def · {c.calls}× ≈{fmt(c.tokens)} ·{' '}
585 {versionOf(s, newest)}
586 </Text>
587 </Box>
588 )
589 })}
590 {ranked.length > tableRoom && <Text dimColor>… {ranked.length - tableRoom} more</Text>}
591 {hints.length > 0 && <Text> </Text>}
592 {hints.length > 0 && <Text bold>What to optimize</Text>}
593 {hints.slice(0, hintRoom).map(hint =>
594 hint.color === undefined ? (
595 <Text dimColor>• {hint.text}</Text>
596 ) : (
597 <Text color={hint.color}>• {hint.text}</Text>
598 ),
599 )}
600 {hints.length > hintRoom && <Text dimColor>… {hints.length - hintRoom} more tips</Text>}
601 </Box>
602 )
603 })
604}
605types/index.d.ts 39 lines1export type ConnectorKind = 'npx' | 'node' | 'command' | 'remote' | 'claude.ai' | 'plugin' | 'unknown'
2
3export type ConnectorTool = { name: string; tokens: number; isLoaded: boolean }
4
5export type ConnectorServer = {
6 name: string
7 kind: ConnectorKind
8 // Where it is configured: .mcp.json, user, local, claude.ai, plugin.
9 scope: string
10 // The npm package it runs, when there is one, and the version spec it asks
11 // npx for (`latest`, `1.2.3`, or null when unpinned).
12 pkg: string | null
13 spec: string | null
14 installed: string | null
15 tools: ConnectorTool[]
16}
17
18export type ConnectorCalls = { calls: number; errors: number; tokens: number; max: number }
19
20export type ConnectorContext = { used: number | null; window: number; mcpLoaded: number }
21
22export type ConnectorHistory = { calls: number; tokens: number; lastAt: number }
23
24declare module 'claude-code' {
25 interface PluginState {
26 'connector-center': {
27 servers: ConnectorServer[]
28 context: ConnectorContext | null
29 // By tool name (mcp__server__tool), this session.
30 calls: Record<string, ConnectorCalls>
31 // By server name, across sessions (mirrors $.store).
32 history: Record<string, ConnectorHistory>
33 latest: Record<string, string>
34 checking: boolean
35 selected: string | null
36 }
37 }
38}
39