[eric] microsoft 365 mcp

This commit is contained in:
ciregenz
2026-04-02 22:38:11 -07:00
parent cac9872702
commit 8fa39ee03a
9 changed files with 2957 additions and 883 deletions
+186 -8
View File
@@ -53,6 +53,7 @@ GOOGLE_SCOPES = [
"https://www.googleapis.com/auth/contacts.readonly",
]
_pending_oauth: dict[str, str] = {}
@@ -419,7 +420,6 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
else:
env = config.setdefault("env", {})
env["OAUTH_ACCESS_TOKEN"] = tool.oauth_tokens["access_token"]
# Notion MCP uses NOTION_TOKEN env var
if tool.name.lower() == "notion":
env["NOTION_TOKEN"] = tool.oauth_tokens["access_token"]
if tool.oauth_tokens.get("refresh_token"):
@@ -431,6 +431,14 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
if client_secret:
env["GOOGLE_WORKSPACE_CLIENT_SECRET"] = client_secret
# Microsoft 365 MCP: use a stable token cache path shared across process spawns
if tool.name.lower() == "microsoft 365" and config.get("type") == "stdio":
env = config.setdefault("env", {})
cache_dir = os.path.join(os.path.expanduser("~"), ".openswarm")
os.makedirs(cache_dir, exist_ok=True)
env["MS365_MCP_TOKEN_CACHE_PATH"] = os.path.join(cache_dir, "ms365-token-cache.json")
env["MS365_MCP_SELECTED_ACCOUNT_PATH"] = os.path.join(cache_dir, "ms365-selected-account.json")
if config.get("type") == "stdio":
if config.get("command"):
# Check for bundled npm MCP servers — use Electron's Node.js instead of npx
@@ -446,8 +454,8 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
config["args"] = [bundle_path]
config.setdefault("env", {})["ELECTRON_RUN_AS_NODE"] = "1"
logger.info(f"Using bundled MCP server for {pkg_name}")
elif electron_path:
# Check for pre-installed npm package
else:
# Check for pre-installed npm package (works in both dev and packaged modes)
safe_dir = pkg_name.replace("/", "-").replace("@", "")
npm_dir = os.path.join(_backend, "npm-servers", safe_dir)
pkg_json_path = os.path.join(npm_dir, "node_modules", pkg_name, "package.json")
@@ -457,10 +465,13 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
pkg_meta = _json.load(f)
bin_field = pkg_meta.get("bin", {})
entry = list(bin_field.values())[0] if isinstance(bin_field, dict) else bin_field
config["command"] = electron_path
config["args"] = [os.path.join(npm_dir, "node_modules", pkg_name, entry)]
config.setdefault("env", {})["ELECTRON_RUN_AS_NODE"] = "1"
logger.info(f"Using pre-installed npm MCP server for {pkg_name}")
node_cmd = electron_path or shutil.which("node")
if node_cmd:
config["command"] = node_cmd
config["args"] = [os.path.join(npm_dir, "node_modules", pkg_name, entry)]
if electron_path:
config.setdefault("env", {})["ELECTRON_RUN_AS_NODE"] = "1"
logger.info(f"Using pre-installed npm MCP server for {pkg_name}")
if not os.path.isabs(config.get("command", "")):
resolved = _resolve_command(config["command"])
@@ -824,13 +835,180 @@ async def discover_tools(tool_id: str):
return {"ok": True, "tool": tool.model_dump()}
# ---------------------------------------------------------------------------
# Microsoft 365 device-code login (runs in the backend, not the MCP server)
# ---------------------------------------------------------------------------
_m365_login_processes: dict[str, dict] = {} # tool_id -> {proc, device_code, status, email}
def _m365_server_script() -> str:
_backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return os.path.join(
_backend, "npm-servers", "softeria-ms-365-mcp-server",
"node_modules", "@softeria", "ms-365-mcp-server", "dist", "index.js",
)
def _m365_cache_env() -> dict[str, str]:
cache_dir = os.path.join(os.path.expanduser("~"), ".openswarm")
os.makedirs(cache_dir, exist_ok=True)
return {
"MS365_MCP_TOKEN_CACHE_PATH": os.path.join(cache_dir, "ms365-token-cache.json"),
"MS365_MCP_SELECTED_ACCOUNT_PATH": os.path.join(cache_dir, "ms365-selected-account.json"),
}
@tools_lib.router.post("/{tool_id}/m365/device-login")
async def m365_device_login(tool_id: str):
"""Start a Microsoft 365 device-code login.
Spawns the MCP server with --login in a long-lived subprocess.
Returns the device code and URL for the user to authenticate.
"""
import subprocess
tool = _load(tool_id)
script = _m365_server_script()
if not os.path.isfile(script):
raise HTTPException(status_code=500, detail="M365 MCP server not installed")
node = shutil.which("node")
electron = os.environ.get("OPENSWARM_ELECTRON_PATH")
cmd = electron or node
if not cmd:
raise HTTPException(status_code=500, detail="No node/electron found")
env = {**os.environ, **_m365_cache_env()}
if electron:
env["ELECTRON_RUN_AS_NODE"] = "1"
# Kill any existing login process for this tool
existing = _m365_login_processes.pop(tool_id, None)
if existing and existing.get("proc"):
try:
existing["proc"].kill()
except Exception:
pass
proc = subprocess.Popen(
[cmd, script, "--login"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env, text=True,
)
# Read stdout lines until we find the device code (MSAL prints it)
import threading
login_state: dict = {"proc": proc, "status": "waiting_for_code", "device_code": "", "device_code_url": "", "email": None, "output": ""}
def _read_output():
import re
for line in proc.stdout:
login_state["output"] += line
# MSAL device code message contains the URL and code
code_match = re.search(r'enter the code\s+(\S+)', line, re.IGNORECASE)
url_match = re.search(r'(https://\S+)', line)
if code_match:
login_state["device_code"] = code_match.group(1)
login_state["status"] = "awaiting_auth"
if url_match and "microsoft" in url_match.group(1).lower():
login_state["device_code_url"] = url_match.group(1)
# Process ended — check result
proc.wait()
remaining_stderr = proc.stderr.read() if proc.stderr else ""
login_state["output"] += remaining_stderr
if proc.returncode == 0:
login_state["status"] = "connected"
# Try to extract email from output
try:
import json as _j
result = _j.loads(login_state["output"].strip().split("\n")[-1])
if result.get("success"):
ud = result.get("userData", {})
login_state["email"] = ud.get("userPrincipalName") or ud.get("displayName")
except Exception:
pass
# Update tool status
try:
t = _load(tool_id)
t.auth_status = "connected"
if login_state.get("email"):
t.connected_account_email = login_state["email"]
_save(t)
except Exception:
pass
else:
login_state["status"] = "error"
thread = threading.Thread(target=_read_output, daemon=True)
thread.start()
_m365_login_processes[tool_id] = login_state
# Wait briefly for device code to appear
for _ in range(30):
if login_state["device_code"]:
break
await asyncio.sleep(0.2)
if not login_state["device_code"]:
return {"status": "error", "message": "Timed out waiting for device code from MCP server"}
return {
"status": "awaiting_auth",
"device_code": login_state["device_code"],
"device_code_url": login_state["device_code_url"] or "https://login.microsoft.com/device",
}
@tools_lib.router.get("/{tool_id}/m365/device-login/status")
async def m365_device_login_status(tool_id: str):
"""Poll the status of a pending M365 device-code login."""
state = _m365_login_processes.get(tool_id)
if not state:
# Check if already connected via cached token
cache_env = _m365_cache_env()
cache_path = cache_env["MS365_MCP_TOKEN_CACHE_PATH"]
if os.path.isfile(cache_path):
tool = _load(tool_id)
if tool.auth_status == "connected":
return {"status": "connected", "email": tool.connected_account_email}
return {"status": "no_login_in_progress"}
status = state["status"]
result: dict = {"status": status}
if status == "connected":
result["email"] = state.get("email")
_m365_login_processes.pop(tool_id, None)
elif status == "error":
result["message"] = "Login failed"
_m365_login_processes.pop(tool_id, None)
return result
@tools_lib.router.post("/{tool_id}/m365/disconnect")
async def m365_disconnect(tool_id: str):
"""Disconnect M365 by clearing the cached token."""
tool = _load(tool_id)
cache_env = _m365_cache_env()
for path in cache_env.values():
if os.path.isfile(path):
os.remove(path)
tool.auth_status = "configured"
tool.connected_account_email = None
_save(tool)
return {"ok": True, "tool": tool.model_dump()}
@tools_lib.router.post("/{tool_id}/oauth/disconnect")
async def oauth_disconnect(tool_id: str):
"""Clear OAuth tokens and reset auth status so the user can reconnect with a different account."""
tool = _load(tool_id)
access_token = tool.oauth_tokens.get("access_token")
if access_token:
if access_token and tool.name.lower() != "notion":
# Revoke Google tokens
try:
async with httpx.AsyncClient(timeout=10.0) as client:
await client.post(
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
{
"name": "softeria-ms-365-mcp-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@softeria/ms-365-mcp-server": "^0.54.1"
}
}
+11 -867
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@electron/notarize": "^3.1.1",
"electron": "castlabs/electron-releases#v33.4.11+wvcus",
"electron": "castlabs/electron-releases#v40.7.0+wvcus",
"electron-builder": "^25.1.0"
},
"build": {
+148 -4
View File
@@ -69,6 +69,9 @@ import {
startOAuth,
fetchToolStatus,
discoverTools,
startDeviceCodeLogin,
pollDeviceCodeStatus,
disconnectM365,
ToolDefinition,
BuiltinTool,
} from '@/shared/state/toolsSlice';
@@ -157,6 +160,20 @@ const INTEGRATIONS: Integration[] = [
),
authType: 'oauth2',
},
{
id: 'microsoft-365',
name: 'Microsoft 365',
description: 'Outlook email, Calendar, OneDrive, Excel, OneNote, Tasks, Contacts, Teams, and SharePoint.',
mcp_config: { type: 'stdio', command: 'npx', args: ['-y', '@softeria/ms-365-mcp-server'] },
color: '#0078D4',
website: 'https://github.com/softeria/ms-365-mcp-server',
icon: (
<svg viewBox="0 0 24 24" width="22" height="22">
<path d="M11.4 24H0V12.6L11.4 24zM24 24H12.6V12.6L24 24zM11.4 11.4H0V0l11.4 11.4zM24 11.4H12.6V0L24 11.4z" fill="#0078D4"/>
</svg>
),
authType: 'device_code' as any,
},
{
id: 'notion',
name: 'Notion',
@@ -439,6 +456,13 @@ const Tools: React.FC = () => {
// Integration toggle state
const [integrationLoading, setIntegrationLoading] = useState<Record<string, boolean>>({});
// Device code login dialog state (M365)
const [deviceCodeDialogOpen, setDeviceCodeDialogOpen] = useState(false);
const [deviceCodeDialogToolId, setDeviceCodeDialogToolId] = useState<string | null>(null);
const [deviceCode, setDeviceCode] = useState('');
const [deviceCodeUrl, setDeviceCodeUrl] = useState('');
const [deviceCodeStatus, setDeviceCodeStatus] = useState<'loading' | 'awaiting' | 'connected' | 'error'>('loading');
// Integration credentials dialog state
const [credDialogOpen, setCredDialogOpen] = useState(false);
const [credDialogToolId, setCredDialogToolId] = useState<string | null>(null);
@@ -482,7 +506,7 @@ const Tools: React.FC = () => {
}));
if (createTool.fulfilled.match(result)) {
const newTool = result.payload;
if (integration.authType === 'oauth2') {
if (integration.authType === 'oauth2' || integration.authType === 'device_code') {
setSnackbar({ open: true, message: `Enabled ${integration.name} — connect your account to discover actions` });
} else {
setSnackbar({ open: true, message: `Enabled ${integration.name} — discovering actions…` });
@@ -815,6 +839,56 @@ const Tools: React.FC = () => {
}
};
const handleDeviceCodeConnect = async (toolId: string) => {
setDeviceCodeDialogToolId(toolId);
setDeviceCodeStatus('loading');
setDeviceCode('');
setDeviceCodeUrl('');
setDeviceCodeDialogOpen(true);
const result = await dispatch(startDeviceCodeLogin(toolId));
if (startDeviceCodeLogin.fulfilled.match(result)) {
const { device_code, device_code_url } = result.payload;
setDeviceCode(device_code);
const url = device_code_url || 'https://login.microsoft.com/device';
setDeviceCodeUrl(url);
setDeviceCodeStatus('awaiting');
// Auto-open Microsoft login in a popup
window.open(url, 'm365-login', 'width=500,height=700,left=200,top=100');
// Poll for completion
const poll = setInterval(async () => {
const statusResult = await dispatch(pollDeviceCodeStatus(toolId));
if (pollDeviceCodeStatus.fulfilled.match(statusResult)) {
const { status, email } = statusResult.payload;
if (status === 'connected') {
clearInterval(poll);
setDeviceCodeStatus('connected');
setSnackbar({ open: true, message: `Connected to Microsoft 365${email ? ` as ${email}` : ''}! Discovering actions…` });
setDeviceCodeDialogOpen(false);
setExpandedToolId(toolId);
await dispatch(fetchToolStatus(toolId));
dispatch(discoverTools(toolId));
} else if (status === 'error') {
clearInterval(poll);
setDeviceCodeStatus('error');
}
}
}, 2000);
// Stop polling after 5 minutes
setTimeout(() => clearInterval(poll), 300000);
} else {
setDeviceCodeStatus('error');
}
};
const handleM365Disconnect = async (toolId: string) => {
await dispatch(disconnectM365(toolId));
setSnackbar({ open: true, message: 'Disconnected from Microsoft 365' });
};
const openCredentialsDialog = (toolId: string, integration: Integration) => {
const tool = items[toolId];
const existing = tool?.credentials || {};
@@ -1431,7 +1505,7 @@ const Tools: React.FC = () => {
</Box>
{tool.description && <Typography sx={{ color: c.text.muted, fontSize: '0.84rem' }}>{tool.description}</Typography>}
</Box>
{!isDisabled && tool.auth_type === 'oauth2' && tool.auth_status !== 'connected' && (
{!isDisabled && (tool.auth_type === 'oauth2' || ig?.authType === 'oauth2') && tool.auth_status !== 'connected' && (
<Button
size="small"
variant="outlined"
@@ -1442,6 +1516,17 @@ const Tools: React.FC = () => {
Connect {tool.name}
</Button>
)}
{!isDisabled && ig?.authType === 'device_code' && tool.auth_status !== 'connected' && (
<Button
size="small"
variant="outlined"
startIcon={<LinkIcon sx={{ fontSize: 14 }} />}
onClick={(e) => { e.stopPropagation(); handleDeviceCodeConnect(tool.id); }}
sx={{ borderColor: `${ig.color}40`, color: ig.color, '&:hover': { borderColor: ig.color, bgcolor: `${ig.color}10` }, textTransform: 'none', fontSize: '0.78rem', borderRadius: 1.5, py: 0.5, flexShrink: 0 }}
>
Connect Microsoft 365
</Button>
)}
{!isDisabled && ig?.credentialFields && tool.auth_status !== 'connected' && (
<Button
size="small"
@@ -1454,12 +1539,12 @@ const Tools: React.FC = () => {
</Button>
)}
{!isDisabled && ig && tool.auth_status === 'connected' && (
<Tooltip title={ig.credentialFields || ig.authType === 'oauth2' ? 'Disconnect' : ''}>
<Tooltip title={ig.credentialFields || ig.authType === 'oauth2' || ig.authType === 'device_code' ? 'Disconnect' : ''}>
<Chip
icon={<CheckCircleIcon sx={{ fontSize: 12 }} />}
label={tool.connected_account_email ? `Connected · ${tool.connected_account_email}` : 'Connected'}
size="small"
onDelete={(ig.credentialFields || ig.authType === 'oauth2') ? (e: React.SyntheticEvent) => { e.stopPropagation(); handleDisconnectIntegration(tool.id, ig); } : undefined}
onDelete={(ig.credentialFields || ig.authType === 'oauth2' || ig.authType === 'device_code') ? (e: React.SyntheticEvent) => { e.stopPropagation(); ig.authType === 'device_code' ? handleM365Disconnect(tool.id) : handleDisconnectIntegration(tool.id, ig); } : undefined}
onClick={(e) => e.stopPropagation()}
sx={{ bgcolor: c.status.successBg, color: c.status.success, fontSize: '0.7rem', height: 22, '& .MuiChip-icon': { color: c.status.success }, '& .MuiChip-deleteIcon': { color: c.status.success, '&:hover': { color: c.status.error } }, flexShrink: 0 }}
/>
@@ -2028,6 +2113,65 @@ const Tools: React.FC = () => {
</DialogActions>
</Dialog>
{/* Microsoft 365 Device Code Login Dialog */}
<Dialog
open={deviceCodeDialogOpen}
onClose={() => { if (deviceCodeStatus !== 'loading') setDeviceCodeDialogOpen(false); }}
maxWidth="xs"
fullWidth
PaperProps={{ sx: { bgcolor: c.bg.surface, backgroundImage: 'none', borderRadius: 4, border: `1px solid ${c.border.subtle}` } }}
>
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ width: 32, height: 32, borderRadius: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: '#0078D418' }}>
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M11.4 24H0V12.6L11.4 24zM24 24H12.6V12.6L24 24zM11.4 11.4H0V0l11.4 11.4zM24 11.4H12.6V0L24 11.4z" fill="#0078D4"/></svg>
</Box>
Connect Microsoft 365
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '8px !important' }}>
{deviceCodeStatus === 'loading' && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 3, justifyContent: 'center' }}>
<CircularProgress size={20} />
<Typography sx={{ color: c.text.muted, fontSize: '0.9rem' }}>Generating login code...</Typography>
</Box>
)}
{deviceCodeStatus === 'awaiting' && (
<>
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', lineHeight: 1.6 }}>
Open the link below and enter the code to sign in:
</Typography>
<Box sx={{ bgcolor: c.bg.page, border: `1px solid ${c.border.subtle}`, borderRadius: 2, p: 2, display: 'flex', flexDirection: 'column', gap: 1.5, alignItems: 'center' }}>
<Typography component="a" href={deviceCodeUrl} target="_blank" rel="noopener" sx={{ color: c.status.info, fontSize: '0.9rem', fontWeight: 500 }}>
{deviceCodeUrl}
</Typography>
<Typography sx={{ fontFamily: c.font.mono, fontSize: '1.5rem', fontWeight: 700, color: c.text.primary, letterSpacing: 2 }}>
{deviceCode}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: 'center', py: 1 }}>
<CircularProgress size={14} />
<Typography sx={{ color: c.text.ghost, fontSize: '0.8rem' }}>Waiting for you to sign in...</Typography>
</Box>
</>
)}
{deviceCodeStatus === 'connected' && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2, justifyContent: 'center' }}>
<CheckCircleIcon sx={{ color: c.status.success, fontSize: 20 }} />
<Typography sx={{ color: c.status.success, fontSize: '0.9rem', fontWeight: 500 }}>Connected successfully!</Typography>
</Box>
)}
{deviceCodeStatus === 'error' && (
<Typography sx={{ color: c.status.error, fontSize: '0.85rem', py: 2, textAlign: 'center' }}>
Login failed. Please try again.
</Typography>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setDeviceCodeDialogOpen(false)} sx={{ color: c.text.muted, textTransform: 'none' }}>
{deviceCodeStatus === 'connected' ? 'Done' : 'Cancel'}
</Button>
</DialogActions>
</Dialog>
{/* Integration Credentials Dialog */}
<Dialog
open={credDialogOpen}
+27
View File
@@ -98,6 +98,33 @@ export const startOAuth = createAsyncThunk(
}
);
export const startDeviceCodeLogin = createAsyncThunk(
'tools/startDeviceCodeLogin',
async (toolId: string) => {
const res = await fetch(`${TOOLS_API}/${toolId}/m365/device-login`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to start device code login');
return await res.json();
}
);
export const pollDeviceCodeStatus = createAsyncThunk(
'tools/pollDeviceCodeStatus',
async (toolId: string) => {
const res = await fetch(`${TOOLS_API}/${toolId}/m365/device-login/status`);
return await res.json();
}
);
export const disconnectM365 = createAsyncThunk(
'tools/disconnectM365',
async (toolId: string) => {
const res = await fetch(`${TOOLS_API}/${toolId}/m365/disconnect`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to disconnect M365');
const data = await res.json();
return data.tool as ToolDefinition;
}
);
export const disconnectOAuth = createAsyncThunk(
'tools/disconnectOAuth',
async (toolId: string) => {
+23 -3
View File
@@ -18,28 +18,36 @@ if [[ -f "$ENV_FILE" ]]; then
fi
PUBLISH_MODE=false
SIGN_MODE=false
if [[ "${1:-}" == "--publish" ]]; then
PUBLISH_MODE=true
SIGN_MODE=true
elif [[ "${1:-}" == "--sign" ]]; then
SIGN_MODE=true
fi
echo "========================================"
echo " OpenSwarm Desktop App Builder"
if $PUBLISH_MODE; then
echo " Mode: PRODUCTION (sign + notarize + publish)"
elif $SIGN_MODE; then
echo " Mode: SIGNED (sign + notarize, no publish)"
else
echo " Mode: LOCAL (unsigned)"
fi
echo "========================================"
echo ""
if $PUBLISH_MODE; then
if $SIGN_MODE; then
missing_vars=()
[[ -z "${APPLE_ID:-}" ]] && missing_vars+=("APPLE_ID")
[[ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]] && missing_vars+=("APPLE_APP_SPECIFIC_PASSWORD")
[[ -z "${APPLE_TEAM_ID:-}" ]] && missing_vars+=("APPLE_TEAM_ID")
[[ -z "${GH_TOKEN:-}" ]] && missing_vars+=("GH_TOKEN")
if $PUBLISH_MODE; then
[[ -z "${GH_TOKEN:-}" ]] && missing_vars+=("GH_TOKEN")
fi
if [[ ${#missing_vars[@]} -gt 0 ]]; then
echo "ERROR: Missing required environment variables for --publish mode:"
echo "ERROR: Missing required environment variables:"
printf ' - %s\n' "${missing_vars[@]}"
echo ""
echo "See script header for details."
@@ -163,7 +171,10 @@ mkdir -p "$STAGING_DIR"
rsync -a \
--exclude='__pycache__' --exclude='**/__pycache__' \
--exclude='*.pyc' --exclude='.venv' \
--exclude='data/tools' \
"$PROJECT_ROOT/backend/" "$STAGING_DIR/backend/"
# Create empty tools directory so the app has a place to write
mkdir -p "$STAGING_DIR/backend/data/tools"
rsync -a \
--exclude='__pycache__' --exclude='**/__pycache__' \
@@ -196,6 +207,15 @@ npm install
if $PUBLISH_MODE; then
npx electron-builder --mac --arm64 --x64 --publish always
elif $SIGN_MODE; then
ARCH=$(uname -m)
if [[ "$ARCH" == "arm64" ]]; then
npx electron-builder --mac --arm64 --publish never
elif [[ "$ARCH" == "x86_64" ]]; then
npx electron-builder --mac --x64 --publish never
else
npx electron-builder --mac --publish never
fi
else
export CSC_IDENTITY_AUTO_DISCOVERY=false
ARCH=$(uname -m)
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# One-time Microsoft 365 authentication for OpenSwarm.
# Run this once to cache your M365 token. After that, M365 works in OpenSwarm automatically.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
CACHE_DIR="$HOME/.openswarm"
mkdir -p "$CACHE_DIR"
export MS365_MCP_TOKEN_CACHE_PATH="$CACHE_DIR/ms365-token-cache.json"
export MS365_MCP_SELECTED_ACCOUNT_PATH="$CACHE_DIR/ms365-selected-account.json"
SERVER_SCRIPT="$PROJECT_ROOT/backend/npm-servers/softeria-ms-365-mcp-server/node_modules/@softeria/ms-365-mcp-server/dist/index.js"
if [ ! -f "$SERVER_SCRIPT" ]; then
echo "M365 MCP server not found. Run 'cd backend/npm-servers/softeria-ms-365-mcp-server && npm install' first."
exit 1
fi
echo ""
echo " Microsoft 365 Login for OpenSwarm"
echo " ─────────────────────────────────"
echo " A browser window will open for you to sign in."
echo " After login, the token is cached and M365 works in OpenSwarm automatically."
echo ""
node "$SERVER_SCRIPT" --login
if [ -f "$MS365_MCP_TOKEN_CACHE_PATH" ]; then
echo ""
echo " ✓ Token cached at $MS365_MCP_TOKEN_CACHE_PATH"
echo " ✓ M365 is ready to use in OpenSwarm!"
echo ""
else
echo ""
echo " ✗ Login may have failed — no token cache found."
echo ""
fi