diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
index 2724f458d84..203c586fe76 100644
--- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
+++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
@@ -538,7 +538,7 @@ export class SettingsComponent extends React.Component<
{nls.localize(
'arduino/preferences/mcp.description',
- 'MCP enables AI assistants like Claude Code to interact with the Arduino IDE - compiling sketches, uploading code, managing libraries, and more.'
+ 'MCP enables AI assistants like Claude Code to work in Arduino Agent with you - compiling sketches, uploading code, managing libraries, and more.'
)}
@@ -618,12 +618,16 @@ export class SettingsComponent extends React.Component<
+ {/* Arduino Agent: the streamable HTTP endpoint (SSE is legacy) and the setup dialog */}
- {nls.localize('arduino/preferences/mcp.connectionInfo', 'To connect Claude Code, add to your MCP configuration:')}
+ {nls.localize('arduino/preferences/mcp.serverAddress', 'MCP server address:')}
- http://127.0.0.1:{this.state.mcpPort}/sse
+ http://127.0.0.1:{this.state.mcpPort}/mcp
+
+ {nls.localize('arduino/preferences/mcp.connectHint', 'For ready-to-paste setups for Claude Code, Claude Desktop and other clients, use Help > Connect an AI Agent...')}
+
);
diff --git a/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts b/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts
index 6e3fe3714bb..a6e2aff753b 100644
--- a/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts
+++ b/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts
@@ -18,6 +18,7 @@ import { bindMCPPreferences, MCPPreferences } from './mcp-preferences';
import { MCPService, MCPServicePath } from '../common/mcp-service';
import { MCPFrontendContribution } from './mcp-frontend-contribution';
import { AgentAbout, DisabledIDEUpdateCheck, RemoveIDEUpdaterCommand } from './agent-branding';
+import { ConnectAgentContribution } from './connect-agent';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Bind MCP preferences to add settings to the IDE preferences panel
@@ -40,5 +41,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MCPFrontendContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MCPFrontendContribution);
+ // Help > Connect an AI Agent..., the MCP status bar item, first-run hint
+ bind(ConnectAgentContribution).toSelf().inSingletonScope();
+ bind(CommandContribution).toService(ConnectAgentContribution);
+ bind(MenuContribution).toService(ConnectAgentContribution);
+ bind(FrontendApplicationContribution).toService(ConnectAgentContribution);
+
console.log('[arduino-mcp] Frontend module loaded with MCP preferences and service proxy');
});
diff --git a/arduino-mcp-extension/src/browser/connect-agent.ts b/arduino-mcp-extension/src/browser/connect-agent.ts
new file mode 100644
index 00000000000..38fe900e1b7
--- /dev/null
+++ b/arduino-mcp-extension/src/browser/connect-agent.ts
@@ -0,0 +1,235 @@
+/**
+ * "Connect an AI Agent": the in-IDE way to hook up Claude Code, Claude Desktop
+ * or any MCP client.
+ *
+ * - Help > Connect an AI Agent... opens a dialog with copy-ready setups for this
+ * installation (real paths of the bundled bridge and of the app, the token).
+ * - A status bar item shows whether the MCP server is up and whether an agent
+ * has used it recently; clicking it opens the same dialog.
+ * - The first time the server is running, a notification points at the dialog.
+ */
+
+import { AbstractDialog } from '@theia/core/lib/browser/dialogs';
+import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
+import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
+import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
+import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
+import { StorageService } from '@theia/core/lib/browser/storage-service';
+import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
+import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu';
+import { MessageService } from '@theia/core/lib/common/message-service';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import {
+ MCPConnectionGuide,
+ MCPConnectionSnippet,
+ MCPService,
+ MCPStatus,
+} from '../common/mcp-service';
+import { MCPFrontendContribution } from './mcp-frontend-contribution';
+
+export namespace ConnectAgentCommands {
+ export const OPEN: Command = {
+ id: 'arduino-agent.connect-ai-agent',
+ label: 'Connect an AI Agent...',
+ category: 'Arduino Agent',
+ };
+}
+
+const STATUS_BAR_ID = 'arduino-agent-mcp-status';
+const STATUS_POLL_MS = 20_000;
+const HINT_SHOWN_KEY = 'arduino-agent.connect-hint-shown';
+
+@injectable()
+export class ConnectAgentContribution
+ implements CommandContribution, MenuContribution, FrontendApplicationContribution
+{
+ @inject(MCPService)
+ private readonly mcpService!: MCPService;
+
+ @inject(MCPFrontendContribution)
+ private readonly mcpFrontend!: MCPFrontendContribution;
+
+ @inject(StatusBar)
+ private readonly statusBar!: StatusBar;
+
+ @inject(ClipboardService)
+ private readonly clipboard!: ClipboardService;
+
+ @inject(MessageService)
+ private readonly messageService!: MessageService;
+
+ @inject(StorageService)
+ private readonly storage!: StorageService;
+
+ registerCommands(registry: CommandRegistry): void {
+ registry.registerCommand(ConnectAgentCommands.OPEN, {
+ execute: () => this.openDialog(),
+ });
+ }
+
+ registerMenus(registry: MenuModelRegistry): void {
+ // Its own group, sorted ahead of Arduino's '0_main' (Getting Started, ...).
+ registry.registerMenuAction([...CommonMenus.HELP, '0_agent'], {
+ commandId: ConnectAgentCommands.OPEN.id,
+ label: ConnectAgentCommands.OPEN.label,
+ order: '0',
+ });
+ }
+
+ onStart(): void {
+ this.mcpFrontend.onDidChangeStatus((status) => this.showStatus(status));
+ void this.refreshStatus().then((status) => this.maybeShowFirstRunHint(status));
+ // Activity expires without any event, so re-read it now and then.
+ setInterval(() => void this.refreshStatus(), STATUS_POLL_MS);
+ }
+
+ private async refreshStatus(): Promise {
+ try {
+ const status = await this.mcpService.getStatus();
+ this.showStatus(status);
+ return status;
+ } catch {
+ return undefined; // backend restarting; the next poll catches up
+ }
+ }
+
+ private showStatus(status: MCPStatus): void {
+ const url = `http://127.0.0.1:${status.port}/mcp`;
+ let text: string;
+ let tooltip: string;
+ if (!status.running && status.error) {
+ text = '$(warning) MCP unavailable';
+ tooltip = status.error;
+ } else if (!status.running) {
+ text = '$(debug-disconnect) MCP off';
+ tooltip = 'The MCP server is off (Preferences > MCP). Click for connection setup.';
+ } else if (status.activeClients > 0) {
+ const n = status.activeClients;
+ text = n === 1 ? '$(hubot) Agent connected' : `$(hubot) ${n} agents connected`;
+ tooltip = `${n} MCP session${n === 1 ? '' : 's'} active in the last 5 minutes on ${url}. Click for connection setup.`;
+ } else {
+ text = '$(hubot) MCP ready';
+ tooltip = `MCP server listening on ${url}. Click to connect an AI agent.`;
+ }
+ this.statusBar.setElement(STATUS_BAR_ID, {
+ alignment: StatusBarAlignment.LEFT,
+ priority: 100,
+ text,
+ tooltip,
+ command: ConnectAgentCommands.OPEN.id,
+ className: 'arduino-agent-mcp-status',
+ });
+ }
+
+ private async maybeShowFirstRunHint(status: MCPStatus | undefined): Promise {
+ if (!status?.running || (await this.storage.getData(HINT_SHOWN_KEY))) {
+ return;
+ }
+ await this.storage.setData(HINT_SHOWN_KEY, true);
+ const show = 'Show me how';
+ const choice = await this.messageService.info(
+ 'Arduino Agent is ready for your AI assistant. Connect Claude Code, Claude Desktop or another MCP client in one step.',
+ show
+ );
+ if (choice === show) {
+ await this.openDialog();
+ }
+ }
+
+ private async openDialog(): Promise {
+ const guide = await this.mcpService.getConnectionGuide();
+ this.showStatus(guide.status);
+ await new ConnectAgentDialog(guide, this.clipboard).open();
+ }
+}
+
+class ConnectAgentDialog extends AbstractDialog {
+ constructor(
+ private readonly guide: MCPConnectionGuide,
+ private readonly clipboard: ClipboardService
+ ) {
+ super({ title: 'Connect an AI Agent', maxWidth: 760, wordWrap: 'break-word' });
+ ensureStyles();
+ this.contentNode.classList.add('arduino-agent-connect');
+ this.contentNode.appendChild(this.renderStatus());
+ for (const snippet of guide.snippets) {
+ this.contentNode.appendChild(this.renderSnippet(snippet));
+ }
+ this.appendAcceptButton('Done');
+ }
+
+ get value(): void {
+ return undefined;
+ }
+
+ private renderStatus(): HTMLElement {
+ const { status, url } = this.guide;
+ const node = element('div', 'arduino-agent-connect-status');
+ const dot = element('span', `arduino-agent-connect-dot ${status.running ? 'on' : 'off'}`);
+ node.appendChild(dot);
+ node.appendChild(
+ document.createTextNode(
+ status.running
+ ? `MCP server running on ${url}` +
+ (status.activeClients ? ` - ${status.activeClients} active session(s)` : '')
+ : status.error ??
+ 'The MCP server is off. Turn it on in Preferences > MCP, then come back here.'
+ )
+ );
+ return node;
+ }
+
+ private renderSnippet(snippet: MCPConnectionSnippet): HTMLElement {
+ const section = element('div', 'arduino-agent-connect-section');
+ const header = element('div', 'arduino-agent-connect-header');
+ header.appendChild(element('div', 'arduino-agent-connect-title', snippet.title));
+ const copy = element('button', 'theia-button secondary arduino-agent-connect-copy', 'Copy') as HTMLButtonElement;
+ copy.addEventListener('click', async () => {
+ await this.clipboard.writeText(snippet.text);
+ copy.textContent = 'Copied';
+ setTimeout(() => (copy.textContent = 'Copy'), 1500);
+ });
+ header.appendChild(copy);
+ section.appendChild(header);
+ section.appendChild(element('div', 'arduino-agent-connect-description', snippet.description));
+ section.appendChild(element('pre', 'arduino-agent-connect-code', snippet.text));
+ return section;
+ }
+}
+
+function element(tag: string, className: string, text?: string): HTMLElement {
+ const node = document.createElement(tag);
+ node.className = className;
+ if (text !== undefined) {
+ node.textContent = text;
+ }
+ return node;
+}
+
+let stylesInstalled = false;
+function ensureStyles(): void {
+ if (stylesInstalled) {
+ return;
+ }
+ stylesInstalled = true;
+ const style = document.createElement('style');
+ style.textContent = `
+.arduino-agent-connect { display: flex; flex-direction: column; gap: 14px; min-width: 560px; }
+.arduino-agent-connect-status { display: flex; align-items: center; gap: 8px; }
+.arduino-agent-connect-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
+.arduino-agent-connect-dot.on { background: var(--theia-testing-iconPassed, #3fb950); }
+.arduino-agent-connect-dot.off { background: var(--theia-errorForeground, #f85149); }
+.arduino-agent-connect-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
+.arduino-agent-connect-title { font-weight: 600; }
+.arduino-agent-connect-copy { min-width: 72px; margin: 0; }
+.arduino-agent-connect-description { opacity: 0.8; margin: 4px 0 6px; }
+.arduino-agent-connect-code {
+ margin: 0; padding: 8px 10px; border-radius: 4px; max-height: 180px; overflow: auto;
+ white-space: pre-wrap; word-break: break-all; user-select: text;
+ font-family: var(--theia-code-font-family, monospace); font-size: var(--theia-code-font-size, 12px);
+ background: var(--theia-textCodeBlock-background, rgba(127, 127, 127, 0.12));
+ border: 1px solid var(--theia-editorWidget-border, rgba(127, 127, 127, 0.3));
+}
+`;
+ document.head.appendChild(style);
+}
diff --git a/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts b/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts
index 48a2024b8b7..3f37e47d51d 100644
--- a/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts
+++ b/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts
@@ -13,6 +13,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { PreferenceService } from '@theia/core/lib/browser/preferences';
import { MessageService } from '@theia/core/lib/common/message-service';
+import { Emitter, Event } from '@theia/core/lib/common/event';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import URI from '@theia/core/lib/common/uri';
import {
@@ -54,6 +55,10 @@ export class MCPFrontendContribution
private recentFileChanges = new Map();
private readonly NOTIFICATION_DEBOUNCE_MS = 2000;
+ private readonly onDidChangeStatusEmitter = new Emitter();
+ /** Server status pushed by the backend (start/stop, sessions opening or closing). */
+ readonly onDidChangeStatus: Event = this.onDidChangeStatusEmitter.event;
+
@postConstruct()
protected init(): void {
// Listen for preference changes
@@ -198,6 +203,7 @@ export class MCPFrontendContribution
*/
onStatusChanged(status: MCPStatus): void {
console.log('[arduino-mcp] MCP status changed:', status);
+ this.onDidChangeStatusEmitter.fire(status);
}
// ============================================================
diff --git a/arduino-mcp-extension/src/common/mcp-service.ts b/arduino-mcp-extension/src/common/mcp-service.ts
index ecc1f67606a..3b5201117cd 100644
--- a/arduino-mcp-extension/src/common/mcp-service.ts
+++ b/arduino-mcp-extension/src/common/mcp-service.ts
@@ -18,9 +18,39 @@ export interface MCPStatus {
enabled: boolean;
running: boolean;
port: number;
+ /** Open sessions. Streamable HTTP clients rarely close theirs, so this only grows. */
connectedClients: number;
+ /** Sessions that made a request within the last ACTIVE_CLIENT_WINDOW_MS. */
+ activeClients: number;
uptime: number; // seconds
authRequired: boolean;
+ /** Why the server is not running although it should be (e.g. its port is taken). */
+ error?: string;
+}
+
+/** How recently a session must have made a request to count as active. */
+export const ACTIVE_CLIENT_WINDOW_MS = 5 * 60 * 1000;
+
+/** One ready-to-paste way of connecting a client. */
+export interface MCPConnectionSnippet {
+ id: 'claude-code' | 'bridge-config' | 'http-config';
+ title: string;
+ description: string;
+ text: string;
+}
+
+/**
+ * Everything needed to connect an MCP client to this installation: the
+ * snippets use the real paths of the bundled bridge and of the app itself.
+ */
+export interface MCPConnectionGuide {
+ status: MCPStatus;
+ url: string;
+ /** The bundled stdio bridge, or null if this build has none. */
+ bridgePath: string | null;
+ /** The app executable to use as ARDUINO_AGENT_PATH, or null in dev builds. */
+ appExecutable: string | null;
+ snippets: MCPConnectionSnippet[];
}
/**
@@ -104,6 +134,12 @@ export interface MCPService {
*/
getClientConfig(): Promise;
+ /**
+ * Connection setups for Claude Code, config-file clients (via the bundled
+ * bridge) and direct HTTP, with this installation's paths filled in.
+ */
+ getConnectionGuide(): Promise;
+
/**
* Check if MCP server is healthy and accepting connections
*/
diff --git a/arduino-mcp-extension/src/node/mcp-server.ts b/arduino-mcp-extension/src/node/mcp-server.ts
index bd5d444496a..7ea00e3e48a 100644
--- a/arduino-mcp-extension/src/node/mcp-server.ts
+++ b/arduino-mcp-extension/src/node/mcp-server.ts
@@ -50,6 +50,7 @@ import { MCP_SERVER_INSTRUCTIONS } from '../common/mcp-instructions';
import { MCP_PROMPTS, findPrompt } from '../common/mcp-prompts';
import { Task } from '../common/mcp-types';
import {
+ ACTIVE_CLIENT_WINDOW_MS,
MCPStatus,
MCPFileChangeEvent,
MCPIDEState,
@@ -391,6 +392,12 @@ export class ArduinoMCPServer {
string,
StreamableHTTPServerTransport
>();
+ // Last request time per session, for MCPStatus.activeClients
+ private readonly sessionLastSeen = new Map();
+ // Notified when a session opens or closes (the IDE's status bar)
+ private sessionsChangedListener: (() => void) | null = null;
+ // Why the last start() failed, shown in the IDE until a start succeeds
+ private startError: string | null = null;
// Task management for async operations
private readonly tasks = new Map();
@@ -446,10 +453,14 @@ export class ArduinoMCPServer {
port: this.port,
connectedClients:
this.sseTransports.size + this.streamableTransports.size,
+ activeClients: [...this.sessionLastSeen.values()].filter(
+ (seen) => Date.now() - seen < ACTIVE_CLIENT_WINDOW_MS
+ ).length,
uptime: this.isRunning
? Math.floor((Date.now() - this.startTime) / 1000)
: 0,
authRequired: this.requireAuth,
+ error: this.isRunning ? undefined : this.startError ?? undefined,
};
}
@@ -480,12 +491,18 @@ export class ArduinoMCPServer {
mcpLog.error(
`Port ${this.port} is in use. Change the arduino.mcp.port preference or set the ARDUINO_MCP_PORT env var.`
);
+ this.startError =
+ `Port ${this.port} is already in use, probably by another running copy of Arduino Agent. ` +
+ 'Close it, or choose another port in Preferences > MCP.';
+ } else {
+ this.startError = `The MCP server could not start: ${err.message}`;
}
reject(err);
});
this.httpServer!.listen(this.port, '127.0.0.1', () => {
this.isRunning = true;
+ this.startError = null;
this.startTime = Date.now();
mcpLog.info(
`MCP Server listening on http://127.0.0.1:${this.port} (auth: ${
@@ -507,6 +524,7 @@ export class ArduinoMCPServer {
await transport.close().catch(() => undefined);
}
this.streamableTransports.clear();
+ this.sessionLastSeen.clear();
await this.serialManager?.disconnect().catch(() => undefined);
return new Promise((resolve) => {
@@ -558,6 +576,27 @@ export class ArduinoMCPServer {
this.fileChangeCallback = callback;
}
+ setSessionsChangedListener(listener: (() => void) | null): void {
+ this.sessionsChangedListener = listener;
+ }
+
+ private touchSession(sessionId: string | undefined): void {
+ if (sessionId) {
+ this.sessionLastSeen.set(sessionId, Date.now());
+ }
+ }
+
+ private sessionOpened(sessionId: string): void {
+ this.touchSession(sessionId);
+ this.sessionsChangedListener?.();
+ }
+
+ private sessionClosed(sessionId: string | undefined): void {
+ if (sessionId && this.sessionLastSeen.delete(sessionId)) {
+ this.sessionsChangedListener?.();
+ }
+ }
+
/**
* A ready-to-paste MCP client configuration snippet.
*/
@@ -727,11 +766,13 @@ export class ArduinoMCPServer {
onsessioninitialized: (sid: string) => {
this.streamableTransports.set(sid, newTransport);
mcpLog.info(`Streamable HTTP session initialized: ${sid}`);
+ this.sessionOpened(sid);
},
});
newTransport.onclose = () => {
if (newTransport.sessionId) {
this.streamableTransports.delete(newTransport.sessionId);
+ this.sessionClosed(newTransport.sessionId);
}
};
const server = this.createMCPServerInstance();
@@ -739,6 +780,7 @@ export class ArduinoMCPServer {
transport = newTransport;
}
+ this.touchSession(sessionId);
await transport.handleRequest(req, res);
}
@@ -751,10 +793,12 @@ export class ArduinoMCPServer {
await server.connect(transport);
this.sseTransports.set(transport.sessionId, transport);
mcpLog.info(`SSE session established: ${transport.sessionId}`);
+ this.sessionOpened(transport.sessionId);
req.on('close', () => {
mcpLog.info(`SSE session closed: ${transport.sessionId}`);
this.sseTransports.delete(transport.sessionId);
+ this.sessionClosed(transport.sessionId);
});
}
@@ -774,6 +818,7 @@ export class ArduinoMCPServer {
res.end(JSON.stringify({ error: 'No active SSE session for this id' }));
return;
}
+ this.touchSession(transport.sessionId);
const chunks: Buffer[] = [];
req.on('data', (chunk) => chunks.push(chunk));
diff --git a/arduino-mcp-extension/src/node/mcp-service-impl.ts b/arduino-mcp-extension/src/node/mcp-service-impl.ts
index b1e3e6b6f10..aa346ac4ebf 100644
--- a/arduino-mcp-extension/src/node/mcp-service-impl.ts
+++ b/arduino-mcp-extension/src/node/mcp-service-impl.ts
@@ -7,6 +7,8 @@
* sketch, board/port selection) pushed from the frontend.
*/
+import * as fs from 'fs';
+import * as path from 'path';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
MCPService,
@@ -14,11 +16,41 @@ import {
MCPServiceClient,
MCPFileChangeEvent,
MCPIDEState,
+ MCPConnectionGuide,
+ MCPConnectionSnippet,
ToolMode,
} from '../common/mcp-service';
import { ArduinoMCPServer } from './mcp-server';
import { mcpLog } from './mcp-logger';
+const BRIDGE_IN_RESOURCES = path.join('mcp-bridge', 'bridge', 'arduino-agent-bridge.js');
+
+/**
+ * The stdio bridge shipped with this build: `resources/mcp-bridge/` in a
+ * release (see electron-app's extraResources), or the extension's own
+ * `bridge/` in a source checkout.
+ */
+function findBridge(): string | null {
+ const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
+ const candidates = [
+ resourcesPath && path.join(resourcesPath, BRIDGE_IN_RESOURCES),
+ // Bundled backend: resources/app/lib/backend -> resources
+ path.resolve(__dirname, '..', '..', '..', BRIDGE_IN_RESOURCES),
+ // Unbundled: arduino-mcp-extension/lib/node -> arduino-mcp-extension/bridge
+ path.resolve(__dirname, '..', '..', 'bridge', 'arduino-agent-bridge.js'),
+ // Bundled in a checkout: electron-app/lib/backend -> repo root
+ path.resolve(__dirname, '..', '..', '..', 'arduino-mcp-extension', 'bridge', 'arduino-agent-bridge.js'),
+ ];
+ return candidates.find((p): p is string => !!p && fs.existsSync(p)) ?? null;
+}
+
+/** The app executable (for ARDUINO_AGENT_PATH), or null when running from a dev Electron. */
+function findAppExecutable(): string | null {
+ return /^arduino[ -](agent|ide)(\.exe)?$/i.test(path.basename(process.execPath))
+ ? process.execPath
+ : null;
+}
+
@injectable()
export class MCPServiceImpl implements MCPService {
@inject(ArduinoMCPServer)
@@ -79,6 +111,58 @@ export class MCPServiceImpl implements MCPService {
return this.mcpServer.buildClientConfig();
}
+ async getConnectionGuide(): Promise {
+ const url = await this.getServerUrl();
+ const token = this.mcpServer.getAuthToken();
+ const bridgePath = findBridge();
+ const appExecutable = findAppExecutable();
+ const quote = (s: string) => `"${s}"`;
+ const snippets: MCPConnectionSnippet[] = [];
+
+ if (bridgePath) {
+ const env = appExecutable ? ` --env ARDUINO_AGENT_PATH=${quote(appExecutable)}` : '';
+ snippets.push({
+ id: 'claude-code',
+ title: 'Claude Code',
+ description:
+ 'Run this in a terminal. It adds Arduino Agent to Claude Code for all your projects, ' +
+ 'through the bundled bridge (needs Node.js 18 or newer).',
+ text: `claude mcp add arduino --scope user${env} -- node ${quote(bridgePath)}`,
+ });
+ const server: Record = { command: 'node', args: [bridgePath] };
+ if (appExecutable) {
+ server.env = { ARDUINO_AGENT_PATH: appExecutable };
+ }
+ snippets.push({
+ id: 'bridge-config',
+ title: 'Claude Desktop, Cursor and other MCP clients',
+ description:
+ 'Add this to the client\'s MCP configuration (claude_desktop_config.json, .mcp.json, ...). ' +
+ 'The bundled bridge stays connected while Arduino Agent is closed and starts it when a ' +
+ 'tool needs it. Needs Node.js 18 or newer.',
+ text: JSON.stringify({ mcpServers: { arduino: server } }, null, 2),
+ });
+ } else {
+ const header = token ? ` --header ${quote(`Authorization: Bearer ${token}`)}` : '';
+ snippets.push({
+ id: 'claude-code',
+ title: 'Claude Code',
+ description: 'Run this in a terminal. It connects while Arduino Agent is running.',
+ text: `claude mcp add arduino --scope user --transport http ${url}${header}`,
+ });
+ }
+ snippets.push({
+ id: 'http-config',
+ title: 'Direct HTTP',
+ description:
+ 'For clients that speak Streamable HTTP. Only connects while Arduino Agent is running' +
+ (token ? '; the token is stored in ~/.arduinoIDE/mcp-token.' : '.'),
+ text: this.mcpServer.buildClientConfig(),
+ });
+
+ return { status: this.mcpServer.getStatus(), url, bridgePath, appExecutable, snippets };
+ }
+
async healthCheck(): Promise {
return this.mcpServer.isServerRunning();
}
@@ -97,6 +181,7 @@ export class MCPServiceImpl implements MCPService {
this.mcpServer.setFileChangeCallback((event) =>
this.notifyFileChanged(event)
);
+ this.mcpServer.setSessionsChangedListener(() => this.notifyStatusChanged());
}
/**