diff --git a/electron/main.js b/electron/main.js
index fc30398d..9cbea494 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -529,3 +529,93 @@ ipcMain.handle('open-external', (_event, url) => {
shell.openExternal(url);
}
});
+
+ipcMain.handle('connect-slack', async () => {
+ const win = new BrowserWindow({
+ width: 900,
+ height: 750,
+ title: 'Sign in to Slack',
+ parent: mainWindow || undefined,
+ modal: false,
+ autoHideMenuBar: true,
+ webPreferences: {
+ partition: 'persist:slack-auth',
+ contextIsolation: true,
+ nodeIntegration: false,
+ sandbox: true,
+ },
+ });
+
+ // Override the global window-open handler so new tabs/windows from Slack
+ // (e.g. workspace redirects) navigate this popup instead of getting
+ // hijacked into a dashboard browser card.
+ win.webContents.setWindowOpenHandler(({ url }) => {
+ if (url.startsWith('http://') || url.startsWith('https://')) {
+ win.loadURL(url).catch(() => {});
+ }
+ return { action: 'deny' };
+ });
+
+ // Block slack:// deep-link attempts (they'd try to launch the native app
+ // and fail). Slack always falls through to a web URL after the deep link
+ // fails, so just swallow these.
+ win.webContents.on('will-navigate', (event, url) => {
+ if (url.startsWith('slack://')) {
+ event.preventDefault();
+ }
+ });
+
+ try {
+ await win.loadURL('https://app.slack.com/signin');
+ } catch (err) {
+ if (!win.isDestroyed()) win.close();
+ throw new Error(`Failed to load Slack: ${err.message}`);
+ }
+
+ return new Promise((resolve, reject) => {
+ let settled = false;
+ const finish = (fn, value) => {
+ if (settled) return;
+ settled = true;
+ clearInterval(pollInterval);
+ clearTimeout(timeoutHandle);
+ if (!win.isDestroyed()) win.close();
+ fn(value);
+ };
+
+ win.on('closed', () => {
+ if (!settled) {
+ settled = true;
+ clearInterval(pollInterval);
+ clearTimeout(timeoutHandle);
+ reject(new Error('Sign-in window was closed'));
+ }
+ });
+
+ const pollInterval = setInterval(async () => {
+ if (win.isDestroyed()) return;
+ try {
+ const token = await win.webContents.executeJavaScript(
+ '(() => { try { return window.boot_data && window.boot_data.api_token; } catch(e) { return null; } })()'
+ );
+ if (typeof token === 'string' && token.startsWith('xoxc-')) {
+ const cookies = await win.webContents.session.cookies.get({ url: 'https://slack.com' });
+ const dCookie = cookies.find((c) => c.name === 'd');
+ if (dCookie && dCookie.value) {
+ // The d cookie value may or may not already include the xoxd- prefix
+ // depending on how Slack encodes it. Normalize it.
+ const raw = decodeURIComponent(dCookie.value);
+ const cookie = raw.startsWith('xoxd-') ? raw : `xoxd-${raw}`;
+ finish(resolve, { token, cookie });
+ }
+ }
+ } catch (_) {
+ // page navigating, ignore
+ }
+ }, 1000);
+
+ const timeoutHandle = setTimeout(() => {
+ finish(reject, new Error('Sign-in timed out after 10 minutes'));
+ }, 10 * 60 * 1000);
+ });
+});
diff --git a/electron/preload.js b/electron/preload.js
index 534c540f..6731cdfb 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -12,6 +12,7 @@ const { contextBridge, ipcRenderer } = require('electron');
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
+ connectSlack: () => ipcRenderer.invoke('connect-slack'),
capturePage: (rect) => ipcRenderer.invoke('capture-page', rect),
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx
index 497ff1ec..cbb36456 100644
--- a/frontend/src/app/pages/Tools/Tools.tsx
+++ b/frontend/src/app/pages/Tools/Tools.tsx
@@ -233,6 +233,28 @@ const INTEGRATIONS: Integration[] = [
),
authType: 'oauth2',
},
+ {
+ id: 'slack',
+ name: 'Slack',
+ description: 'Search messages, send messages, read channels, DMs, and threads in Slack workspaces.',
+ mcp_config: { type: 'stdio', command: 'npx', args: ['-y', 'slack-mcp-server@latest', '--transport', 'stdio'], env: { SLACK_MCP_ADD_MESSAGE_TOOL: 'true' } },
+ color: '#4A154B',
+ website: 'https://github.com/korotovsky/slack-mcp-server',
+ icon: (
+
+ ),
+ connectLabel: 'Connect Slack',
+ connectInstructions: 'On macOS with Chrome, tokens are auto-extracted. Otherwise: open app.slack.com in Chrome → F12 → Console → type `JSON.stringify({token: boot_data.api_token, cookie: document.cookie.match(/d=([^;]+)/)?.[1]})` → copy the result.',
+ credentialFields: [
+ { key: 'SLACK_MCP_XOXC_TOKEN', label: 'Slack Token (xoxc-...)', placeholder: 'Auto-detected via Sign in, or paste xoxc- token' },
+ { key: 'SLACK_MCP_XOXD_TOKEN', label: 'Slack Cookie (xoxd-...)', placeholder: 'Auto-detected via Sign in, or paste xoxd- cookie' },
+ ],
+ },
];
const CATEGORY_ORDER = ['filesystem', 'system', 'search', 'interaction', 'agents', 'planning', 'scheduling'];
@@ -972,6 +994,37 @@ const Tools: React.FC = () => {
}
};
+ const handleSlackAutoConnect = async () => {
+ if (!credDialogToolId || !credDialogIntegration) return;
+ const slackBridge = (window as any).openswarm?.connectSlack;
+ if (!slackBridge) {
+ setSnackbar({ open: true, message: 'Slack auto-connect requires the desktop app', severity: 'error' });
+ return;
+ }
+ setCredDialogSaving(true);
+ try {
+ const { token, cookie } = await slackBridge();
+ const creds = { SLACK_MCP_XOXC_TOKEN: token, SLACK_MCP_XOXD_TOKEN: cookie };
+ const result = await dispatch(updateTool({
+ id: credDialogToolId,
+ credentials: creds,
+ auth_type: 'env_vars',
+ auth_status: 'connected',
+ }));
+ if (updateTool.fulfilled.match(result)) {
+ setCredDialogOpen(false);
+ setSnackbar({ open: true, message: 'Slack connected! Re-discovering actions…' });
+ dispatch(discoverTools(credDialogToolId));
+ } else {
+ setSnackbar({ open: true, message: 'Failed to save Slack credentials', severity: 'error' });
+ }
+ } catch (err: any) {
+ setSnackbar({ open: true, message: err?.message || 'Slack sign-in cancelled', severity: 'error' });
+ } finally {
+ setCredDialogSaving(false);
+ }
+ };
+
const handleDisconnectIntegration = async (toolId: string, integration: Integration) => {
if (integration.authType === 'oauth2') {
// Revoke the token on Google's side (fire-and-forget)
@@ -2237,35 +2290,43 @@ const Tools: React.FC = () => {
{credDialogIntegration?.connectLabel || 'Connect'}
- {credDialogIntegration?.connectInstructions && (
+ {credDialogIntegration?.id === 'slack' ? (
- {credDialogIntegration.connectInstructions}
+ Click Sign in with Slack below — a Slack window will open. Sign in normally and the window will close automatically once you reach your workspace.
+ ) : (
+ <>
+ {credDialogIntegration?.connectInstructions && (
+
+ {credDialogIntegration.connectInstructions}
+
+ )}
+ {(credDialogIntegration?.credentialFields || []).map((field) => (
+ setCredDialogValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
+ fullWidth
+ size="small"
+ helperText={field.helpText}
+ sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.page, fontFamily: c.font.mono, fontSize: '0.85rem' } }}
+ />
+ ))}
+ >
)}
- {(credDialogIntegration?.credentialFields || []).map((field) => (
- setCredDialogValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
- fullWidth
- size="small"
- helperText={field.helpText}
- sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.page, fontFamily: c.font.mono, fontSize: '0.85rem' } }}
- />
- ))}