Lets a session reconnect an MCP server that broke (one by name, or all) once the current turn ends, as /mcp reconnect does, then resume on its own.

Lets a session reconnect one of its MCP servers once the current turn ends, as /mcp reconnect <server> does, then carry on by itself. Useful when a server's tools stop answering and a reconnect is all it needs: a browser extension that dropped its connection, a local server that crashed or timed out. Part of claude-code-self-tools.
reconnect_mcp_after_turn (Claude sees mcp__self-mcp__reconnect_mcp_after_turn):
server: the server's name as /mcp lists it, or all for every server that isn't connected. For a tool named mcp__<server>__<tool>, the name is usually <server>.resume (optional): the message that starts the session's next turn once the reconnect has run, saying what to try again./mcp reconnect <server> runs once the turn ends with Claude's answer, and shows in the transcript as if you had typed it. That is the typed form of the /mcp menu's Reconnect, so no menu opens and nothing waits on you./mcp reconnect prints whether it reconnected (for example Successfully reconnected to claude-in-chrome) in the transcript, where you and Claude both read it. It hands the plugin no text, so the toast and the resume point to that line. With resume, the session sends itself "MCP reconnect of <server> ran: … Resume: …", which starts a new turn. Without resume, Claude is told on its next turn and the session waits for your message./mcp./plugin marketplace add moefingers/claude-code-self-tools
/plugin install self-mcp@claude-code-self-toolshooks/register.ts 143 lines1// A session reconnects one of its MCP servers that broke, or all of them, once the current turn
2// ends, as `/mcp reconnect <server>` does, and can carry on by itself afterwards. The model calls
3// reconnect_mcp_after_turn; a command can't run while the turn the model is in waits on it, so the
4// call is queued and the reconnect runs once the turn has ended with an answer. An interrupted turn
5// cancels it. Only the main conversation reconnects this way, never a subagent.
6//
7// `/mcp reconnect` is the typed form of the /mcp menu's Reconnect: it opens no menu, so it runs
8// unattended. Its outcome (reconnected, failed, no such server) is its own transcript line, as if
9// typed; the toast and the resume, or a note the model reads, point to it.
10import type { EngineInterface, Register } from 'claude-code'
11import type { Queued } from '../types'
12
13const queued = { plugin: 'self-mcp', key: 'queued' } as const
14
15/** A beat after the turn's end, so the turn is no longer running when the command starts. */
16const SETTLE_MS = 500
17
18const DESCRIPTION = [
19 'Reconnects one of this session\'s MCP servers once the current turn ends, as /mcp reconnect does: for a server whose tools stopped answering (a browser extension that dropped, a server that crashed or timed out), where reconnecting is all it needs.',
20 '`server` is its name as /mcp lists it; for a tool named mcp__<server>__<tool> that is usually <server>. `all` reconnects every server that is not connected.',
21 'Not for a server that needs sign-in, or one turned off: /mcp is the person\'s for those.',
22 '`resume` is the message that starts your next turn once the reconnect has run, with what the reconnect said: what to try again.',
23 'Give it whenever you have a next step of your own; leave it out only when the next step waits on someone else (the user, another session, a check).',
24 'End the turn after calling it; the reconnect runs once your answer is shown.',
25].join(' ')
26
27/** Trimmed text, or undefined when it is not a string or is blank. */
28function text(raw: unknown): string | undefined {
29 const trimmed = typeof raw === 'string' ? raw.trim() : ''
30 return trimmed === '' ? undefined : trimmed
31}
32
33/** Why a call was refused, as one line. */
34function why(err: unknown): string {
35 const said = (err instanceof Error ? err.message : String(err)).trim()
36 return said === '' ? 'no reason given' : said
37}
38
39/** One line: the command's output can run over several. */
40const oneLine = (said: string) => said.replace(/\s*\n\s*/g, ' ')
41
42/** What is queued: `resume` only when there is one, since stored state holds no undefined. */
43const entry = (server: string, resume: string | undefined): Queued => (resume === undefined ? { server } : { server, resume })
44
45/** Tells the model on its next turn, in a row it reads and the person doesn't see as typed. */
46async function tellModel($: EngineInterface, note: string): Promise<void> {
47 try {
48 await $.session.append({ message: { type: 'user', content: [{ type: 'text', text: note }] } })
49 } catch {
50 // The toast has told the person; there is no other way to reach the model.
51 }
52}
53
54async function sendResume($: EngineInterface, said: string, resume: string): Promise<void> {
55 try {
56 const sent = await $.prompt.submit({ text: `${said} Resume: ${resume}` })
57 if (sent.drop !== undefined) $.ui.toast(`Resume not sent: ${sent.drop}`)
58 } catch {
59 $.ui.toast('Resume not sent; the session waits for the next message.')
60 }
61}
62
63/** Runs the reconnect, then tells the person and the model what it said. */
64async function reconnectNow($: EngineInterface, value: Queued): Promise<void> {
65 $.ui.status(undefined)
66 let said: string
67 try {
68 const output = text((await $.command.run({ command: 'mcp', args: `reconnect ${value.server}` })).text)
69 // /mcp shows its outcome as its own transcript line, which the person and the model both read,
70 // and hands the caller no text; a version that hands one back has it passed on.
71 said =
72 output === undefined
73 ? `MCP reconnect of ${value.server} ran: Claude Code's own line above says whether it reconnected.`
74 : `MCP reconnect of ${value.server}: ${oneLine(output)}`
75 } catch (err) {
76 said = `MCP reconnect of ${value.server} didn't run: ${why(err)}.`
77 }
78 $.ui.toast(said)
79 if (value.resume !== undefined) await sendResume($, said, value.resume)
80 else await tellModel($, said)
81}
82
83export const register: Register = on => {
84 on('session.start', async ($, e, next) => {
85 const started = await next(e)
86 await $.tool.register({
87 name: 'reconnect_mcp_after_turn',
88 description: DESCRIPTION,
89 inputSchema: {
90 type: 'object',
91 properties: {
92 server: {
93 type: 'string',
94 description: 'The MCP server to reconnect, by its name as /mcp lists it (usually the <server> in mcp__<server>__<tool>), or `all`.',
95 },
96 resume: {
97 type: 'string',
98 description:
99 'The message that starts your next turn once the reconnect has run: what to try again. Leave it out only when the next step waits on someone else.',
100 },
101 },
102 required: ['server'],
103 additionalProperties: false,
104 },
105 isDeferred: false,
106 })
107 return started
108 })
109
110 on('tool.call', { tool: 'mcp__self-mcp__reconnect_mcp_after_turn' }, async ($, e) => {
111 if (e.agentId !== undefined) {
112 return {
113 deny: "A subagent can't reconnect the session's MCP servers. Finish your piece and return its result, saying which server needs a reconnect: the main session can reconnect it.",
114 }
115 }
116 const input = e as { server?: unknown; resume?: unknown }
117 const server = text(input.server)
118 if (server === undefined || /[\r\n]/.test(server)) {
119 return { deny: '`server` must be one MCP server\'s name as /mcp lists it, or `all`, on one line.' }
120 }
121 const resume = text(input.resume)
122 await $.state.set(queued, entry(server, resume))
123 $.ui.status(`MCP reconnect of ${server} queued for when this turn ends`)
124 const then = resume === undefined ? 'then waits for the next message' : `then resumes with: ${resume}`
125 return { result: `Queued. ${server} reconnects once this turn ends, and the session ${then}` }
126 }).catch(($, e, next) => (next.called ? next(e) : { deny: 'self-mcp: queuing the reconnect failed; nothing is queued.' }))
127
128 on('turn.complete', async ($, e, next) => {
129 const done = await next(e)
130 if (e.agentId !== undefined) return done
131 const { value } = await $.state.get(queued)
132 if (!value) return done
133 await $.state.set(queued, null)
134 if (e.isAborted || e.reason !== 'answer') {
135 $.ui.status(undefined)
136 $.ui.toast("MCP reconnect cancelled: the turn didn't finish.")
137 return done
138 }
139 $.clock.after(SETTLE_MS, () => void reconnectNow($, value))
140 return done
141 })
142}
143types/index.d.ts 13 lines1/**
2 * A reconnect the session asked for, waiting for its turn to end: the server, as /mcp lists it or
3 * `all`, and the message it resumes with once the reconnect has run (absent: it waits for the next
4 * message).
5 */
6export type Queued = { server: string; resume?: string }
7
8declare module 'claude-code' {
9 interface PluginState {
10 'self-mcp': { queued: Queued | null }
11 }
12}
13