Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/MCP_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,25 @@ export async function invokeHeadless(input: Record<string, unknown>, context: {

The globals are installed directly on `globalThis`, so tool code written for windowed execution (`window.dataverseAPI.queryData(...)`) continues to work unchanged because `window` is aliased to `globalThis` by the headless runtime.

### Headless Tool Logs

Headless invocations now capture tool messages into the live job record so the MCP Server page can drill into an invocation and show its runtime output.

The supported logging surface is the `context.logger` object passed to `invokeHeadless(...)`:

```typescript
export async function invokeHeadless(
input: Record<string, unknown>,
context: { logger: { debug(message: string): void; info(message: string): void; warn(message: string): void; error(message: string): void } },
) {
context.logger.info("Starting headless work");
context.logger.warn("Using fallback data source");
context.logger.error("Something failed");
}
```

These messages are stored with the job, surfaced in the MCP Server detail pane, and kept bounded so the log history does not grow without limit.

## Tool Runtime Context

When a tool is launched by MCP, `toolboxAPI.invocation.getLaunchContext()` returns the prefill data. If present, invocation metadata is attached under `__pptb`.
Expand Down Expand Up @@ -224,3 +243,4 @@ The runtime currently:
- Use MCP Inspector as the manual test harness for this feature area.
- Verify that `list-tools` shows the supported modes and that `call-tool` returns the expected one-way or two-way response.
- Check that invocation logs redact connection-related identifiers and sensitive payload fields.
- In the PPTB renderer, the MCP Server page now refreshes live and supports drilling into a selected invocation to inspect the captured job logs, progress, result, and error state.
4 changes: 4 additions & 0 deletions src/common/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,15 @@ export const PROTOCOL_CHANNELS = {
// Agent Invocation Logging channels
export const AGENT_INVOCATION_CHANNELS = {
GET_LOGS: "agent-invocation:get-logs",
CLEAR_LOGS: "agent-invocation:clear-logs",
} as const;

// MCP server status/details channels
export const MCP_SERVER_CHANNELS = {
GET_DETAILS: "mcp-server:get-details",
GET_CLIENT_CONFIG_STATUSES: "mcp-server:get-client-config-statuses",
GET_JOB_STATUS: "mcp-server:get-job-status",
CLEAR_LOGS: "mcp-server:clear-logs",
START: "mcp-server:start",
STOP: "mcp-server:stop",
CONFIGURE_CLAUDE_DESKTOP: "mcp-server:configure-claude-desktop",
Expand Down
44 changes: 42 additions & 2 deletions src/common/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,48 @@ export interface AgentInvocationLogEntry {
toolName: string;
connectionId: string | null;
prefillSummary: string;
outcome: "completed" | "no-result" | "rejected";
outcome: "in-progress" | "completed" | "no-result" | "rejected";
invocationMode?: "one-way" | "two-way";
correlationId?: string;
correlationId: string;
error?: string;
}

/**
* Log entry captured for a headless MCP job.
*/
export interface HeadlessJobLogEntry {
timestamp: string;
level: "debug" | "info" | "warn" | "error";
message: string;
}

/**
* Headless MCP job details shown in the renderer drill-down view.
*/
export interface HeadlessJobDetails {
jobId: string;
toolId: string;
toolName: string;
status: "pending" | "in_progress" | "completed" | "failed";
createdAt: string;
startedAt?: string;
completedAt?: string;
timeoutMs: number;
progress?: {
percent: number;
message?: string;
};
result?: Record<string, unknown>;
error?: string;
logs?: HeadlessJobLogEntry[];
}

/**
* Agent Invocation API namespace
*/
export interface AgentInvocationAPI {
getLogs: () => Promise<AgentInvocationLogEntry[]>;
clearLogs: () => Promise<void>;
}

/**
Expand All @@ -119,11 +150,20 @@ export interface McpClientConfigWriteResult {
serverName: string;
}

export interface McpClientConfigStatus {
client: "claude-desktop" | "vscode";
status: "connected" | "not-configured" | "invalid";
filePath: string;
}

/**
* MCP server API namespace
*/
export interface McpServerAPI {
getDetails: () => Promise<McpServerDetails>;
getClientConfigStatuses: () => Promise<McpClientConfigStatus[]>;
getJobStatus: (jobId: string) => Promise<HeadlessJobDetails | null>;
clearLogs: () => Promise<void>;
start: () => Promise<McpServerDetails>;
stop: () => Promise<McpServerDetails>;
configureClaudeDesktop: () => Promise<McpClientConfigWriteResult>;
Expand Down
1 change: 1 addition & 0 deletions src/common/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum ToolBoxEvent {
TERMINAL_OUTPUT = "terminal:output",
TERMINAL_COMMAND_COMPLETED = "terminal:command:completed",
TERMINAL_ERROR = "terminal:error",
MCP_HEADLESS_JOB_UPDATED = "mcp:headless-job-updated",
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/common/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export interface UserSettings {
machineId?: string; // @deprecated - legacy machine identifier retained for migrations
pendingWhatsNewVersion?: string | null; // Version whose What's New should be shown after restart (auto-update)
restoreSessionOnStartup?: boolean; // Whether to reopen previously open tools on app start
keepMcpServerRunning?: boolean; // Keep MCP server running by auto-starting it when tools open/reopen
// Sort preferences
installedToolsSort?: InstalledToolsSortOption;
connectionsSort?: ConnectionsSortOption;
Expand Down
115 changes: 109 additions & 6 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ import { ToolManager } from "./managers/toolsManager";
import { ToolWindowManager } from "./managers/toolWindowManager";
import { TrayManager } from "./managers/trayManager";
import { VersionManager } from "./managers/versionManager";
import { readLogEntries } from "./mcp/agentInvocationLogger";
import { clearLogEntries, readLogEntries } from "./mcp/agentInvocationLogger";
import { McpServerManager } from "./mcp/mcpServer";
import { ActiveToolInfo, buildToolBoxFeedbackUrl, buildToolFeedbackUrl, getEnvironmentDiagnostics, resolveActiveToolInfo } from "./utilities";
import { applyMainSentryConsent } from "./sentryRuntime";
import { ActiveToolInfo, buildToolBoxFeedbackUrl, buildToolFeedbackUrl, getEnvironmentDiagnostics, resolveActiveToolInfo } from "./utilities";

// Constants
const MENU_CREATION_DEBOUNCE_MS = 150; // Debounce delay for menu recreation during rapid tool switches
Expand All @@ -67,6 +67,11 @@ const FAVICON_MAX_BYTES = 65536; // 64 KB — more than enough for any favicon
const OPEN_EXTERNAL_ALLOWED_PROTOCOLS = new Set<string>(["https:", "http:", "mailto:"]);
const OPEN_IN_CONNECTION_BROWSER_ALLOWED_PROTOCOLS = new Set<string>(["https:", "http:"]);

function isExpectedAutoUpdateUnavailableError(errorMessage: string): boolean {
const normalized = errorMessage.toLowerCase();
return normalized.includes("only available in packaged releases") || normalized.includes("updater metadata is missing") || normalized.includes("only supported for the windows nsis");
}

const isFaviconAllowedHost = (hostname: string): boolean => {
if (FAVICON_ALLOWED_HOSTS.has(hostname)) return true;
return FAVICON_ALLOWED_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix));
Expand Down Expand Up @@ -100,6 +105,7 @@ class ToolBoxApp {
private menuCreationTimeout: NodeJS.Timeout | null = null; // Debounce timer for menu recreation
private isQuitting = false; // True once the user explicitly quits (e.g. tray "Quit" or Cmd+Q)
private shouldFocusAfterWindowCreation = false; // Tracks a relaunch request before main window exists
private mcpAutoStartInProgress = false;

/**
* Resolve the application icon for the current release channel.
Expand All @@ -123,7 +129,10 @@ class ToolBoxApp {
try {
this.settingsManager = new SettingsManager();
this.installIdManager = new InstallIdManager(this.settingsManager);
void applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
void applyMainSentryConsent(
this.settingsManager.getSentryTelemetryConsent(),
this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined,
);

this.connectionsManager = new ConnectionsManager();
this.api = new ToolBoxUtilityManager();
Expand All @@ -147,6 +156,15 @@ class ToolBoxApp {
this.toolFilesystemAccessManager = new ToolFileSystemAccessManager();
this.mcpServerManager = new McpServerManager(7339, "127.0.0.1", this.settingsManager, this.toolManager.getRegistryManager(), this.toolManager);
this.mcpServerManager.setConnectionAuthManagers(this.connectionsManager, this.authManager);
this.mcpServerManager.setJobChangeHandler((jobId) => {
if (this.mainWindow) {
this.mainWindow.webContents.send(EVENT_CHANNELS.TOOLBOX_EVENT, {
event: ToolBoxEvent.MCP_HEADLESS_JOB_UPDATED,
data: { jobId },
timestamp: new Date().toISOString(),
});
}
});
this.trayManager = new TrayManager(
() => this.mainWindow,
() => this.createWindow(),
Expand Down Expand Up @@ -277,6 +295,11 @@ class ToolBoxApp {
});

this.autoUpdateManager.on("update-error", (error) => {
if (error?.message && isExpectedAutoUpdateUnavailableError(error.message)) {
logInfo(`[AutoUpdate] Suppressing expected update-unavailable notification: ${error.message}`);
return;
}

this.api.showNotification({
title: "Update Error",
body: `Failed to check for updates: ${error.message}`,
Expand Down Expand Up @@ -462,9 +485,16 @@ class ToolBoxApp {

// Agent invocation logging handlers
ipcMain.removeHandler(AGENT_INVOCATION_CHANNELS.GET_LOGS);
ipcMain.removeHandler(AGENT_INVOCATION_CHANNELS.CLEAR_LOGS);
ipcMain.removeHandler(AGENT_INVOCATION_CHANNELS.CLEAR_LOGS);

// MCP server handlers
ipcMain.removeHandler(MCP_SERVER_CHANNELS.GET_DETAILS);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.GET_CLIENT_CONFIG_STATUSES);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.GET_JOB_STATUS);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.CLEAR_LOGS);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.GET_JOB_STATUS);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.CLEAR_LOGS);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.START);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.STOP);
ipcMain.removeHandler(MCP_SERVER_CHANNELS.CONFIGURE_CLAUDE_DESKTOP);
Expand Down Expand Up @@ -509,7 +539,10 @@ class ToolBoxApp {
ipcMain.handle(SETTINGS_CHANNELS.UPDATE_USER_SETTINGS, async (_, settings) => {
this.settingsManager.updateUserSettings(settings);
if (Object.prototype.hasOwnProperty.call(settings, "sentryTelemetryConsent")) {
await applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
await applyMainSentryConsent(
this.settingsManager.getSentryTelemetryConsent(),
this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined,
);
}
this.api.emitEvent(ToolBoxEvent.SETTINGS_UPDATED, settings);
});
Expand All @@ -521,7 +554,10 @@ class ToolBoxApp {
ipcMain.handle(SETTINGS_CHANNELS.SET_SETTING, async (_, key, value) => {
this.settingsManager.setSetting(key, value);
if (key === "sentryTelemetryConsent") {
await applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
await applyMainSentryConsent(
this.settingsManager.getSentryTelemetryConsent(),
this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined,
);
}
});

Expand All @@ -537,13 +573,45 @@ class ToolBoxApp {

// Agent invocation logs (main UI only)
ipcMain.handle(AGENT_INVOCATION_CHANNELS.GET_LOGS, () => {
return readLogEntries();
const persistedLogs = readLogEntries();
const persistedCorrelationIds = new Set(persistedLogs.flatMap((entry) => (entry.correlationId ? [entry.correlationId] : [])));
const activeLogs = this.mcpServerManager
.getActiveJobs()
.filter((job) => !persistedCorrelationIds.has(job.jobId))
.map((job) => ({
timestamp: job.createdAt,
toolId: job.toolId,
toolName: job.toolName,
connectionId: null,
prefillSummary: "",
outcome: "in-progress" as const,
correlationId: job.jobId,
}));

return [...activeLogs, ...persistedLogs].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
});

ipcMain.handle(AGENT_INVOCATION_CHANNELS.CLEAR_LOGS, () => {
clearLogEntries();
});

ipcMain.handle(MCP_SERVER_CHANNELS.GET_DETAILS, () => {
return this.mcpServerManager.getServerDetails();
});

ipcMain.handle(MCP_SERVER_CHANNELS.GET_CLIENT_CONFIG_STATUSES, async () => {
return await this.mcpServerManager.getClientConfigStatuses();
});

ipcMain.handle(MCP_SERVER_CHANNELS.GET_JOB_STATUS, (_, jobId: string) => {
return this.mcpServerManager.getJobStatus(jobId);
});

ipcMain.handle(MCP_SERVER_CHANNELS.CLEAR_LOGS, () => {
clearLogEntries();
this.mcpServerManager.clearLogs();
});

ipcMain.handle(MCP_SERVER_CHANNELS.START, async () => {
await this.mcpServerManager.start();
this.trayManager?.refreshContextMenu();
Expand Down Expand Up @@ -2940,6 +3008,9 @@ class ToolBoxApp {
this.toolWindowManager.setOnActiveToolChanged(() => {
this.debouncedCreateMenu();
});
this.toolWindowManager.setOnToolLaunched(async () => {
await this.ensureMcpServerRunningForMode("tool-launch");
});

// Initialize NotificationWindowManager for overlay notifications
this.notificationWindowManager = new NotificationWindowManager(this.mainWindow, this.settingsManager);
Expand All @@ -2961,6 +3032,7 @@ class ToolBoxApp {
// After the renderer is ready, auto-open What's New if an auto-update was installed.
this.mainWindow.webContents.once("did-finish-load", () => {
this.openWhatsNewIfPending();
void this.ensureMcpServerRunningForMode("startup");
});

// Open DevTools in development
Expand Down Expand Up @@ -2998,6 +3070,37 @@ class ToolBoxApp {
}
}

private async ensureMcpServerRunningForMode(mode: "tool-launch" | "startup"): Promise<void> {
const keepMcpServerRunning = this.settingsManager.getSetting("keepMcpServerRunning");
if (!keepMcpServerRunning || this.mcpServerManager.isRunning() || this.mcpAutoStartInProgress) {
return;
}

this.mcpAutoStartInProgress = true;
try {
await this.mcpServerManager.start();
this.trayManager?.refreshContextMenu();
const body =
mode === "startup"
? "MCP server was automatically started at startup because Keep MCP Server Running is enabled."
: "MCP server was automatically started because Keep MCP Server Running is enabled.";
this.api.showNotification({
title: "MCP Server Started",
body,
type: "success",
});
} catch (error) {
logError(`[MCP] Failed to auto-start server (${mode})`, error);
this.api.showNotification({
title: "MCP Server Auto-Start Failed",
body: mode === "startup" ? "Unable to automatically start MCP server at startup." : "Unable to automatically start MCP server after tool launch.",
type: "error",
});
} finally {
this.mcpAutoStartInProgress = false;
}
}

/**
* Show About dialog with version and environment info
* Includes install ID and other important information for diagnostics
Expand Down
Loading
Loading