mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 13:34:50 +02:00
[eric] notion integration
This commit is contained in:
@@ -17,6 +17,9 @@ electron/package-lock.json
|
||||
9router/.next/
|
||||
9router/package-lock.json
|
||||
|
||||
# Pre-installed MCP server dependencies
|
||||
backend/npm-servers/*/node_modules/
|
||||
|
||||
# Frontend build output
|
||||
frontend/dist/
|
||||
# Bundled uv binaries (downloaded during build)
|
||||
|
||||
@@ -1593,7 +1593,7 @@ class AgentManager:
|
||||
resp = await client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=30,
|
||||
system="Generate a clear 3-5 word title for this chat. Use plain language like 'Debug Login Page', 'Weekly Report Draft', 'API Integration Help'. No quotes, no punctuation, no emojis. Return only the title.",
|
||||
system="Generate a clear 3-5 word title describing what the user wants to do. Examples: 'Summarize Today Emails', 'Debug Login Page', 'Weekly Report Draft'. Never describe errors, outcomes, or system states. No quotes, no punctuation, no emojis. Return only the title.",
|
||||
messages=[{"role": "user", "content": first_prompt}],
|
||||
)
|
||||
generated = resp.content[0].text.strip().strip('"\'')
|
||||
|
||||
@@ -155,6 +155,29 @@ BROWSER_TOOLS_SCHEMA = [
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "RequestHumanIntervention",
|
||||
"description": (
|
||||
"Request the user's help when you encounter an obstacle you cannot solve "
|
||||
"programmatically — captchas, login prompts, cookie consent walls, "
|
||||
"two-factor authentication, or any blocking popup. The agent will pause "
|
||||
"until the user resolves the issue and clicks Continue."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"problem": {
|
||||
"type": "string",
|
||||
"description": "What obstacle was encountered.",
|
||||
},
|
||||
"instruction": {
|
||||
"type": "string",
|
||||
"description": "What the user should do to resolve it.",
|
||||
},
|
||||
},
|
||||
"required": ["problem", "instruction"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
ACTION_MAP = {
|
||||
@@ -188,8 +211,9 @@ SYSTEM_PROMPT = (
|
||||
"you've reached the end of the page.\n"
|
||||
"- For complex SPAs (Notion, Gmail, etc.), prefer BrowserScroll over BrowserEvaluate for scrolling.\n"
|
||||
"- Avoid looping: if scrolling shows no new content (scrolled 0px), you're at the boundary.\n\n"
|
||||
"You have access ONLY to browser tools. Do not ask the user questions — "
|
||||
"complete the task autonomously to the best of your ability."
|
||||
"You have access ONLY to browser tools. Complete the task autonomously. "
|
||||
"If you encounter a captcha, login wall, or popup you cannot bypass, "
|
||||
"use RequestHumanIntervention to ask the user for help instead of retrying endlessly."
|
||||
)
|
||||
|
||||
MAX_TURNS = 25
|
||||
@@ -427,6 +451,30 @@ async def run_browser_agent(
|
||||
cancelled = True
|
||||
break
|
||||
|
||||
# Handle RequestHumanIntervention — pause and wait for user
|
||||
if tu.name == "RequestHumanIntervention":
|
||||
problem = tu.input.get("problem", "")
|
||||
instruction = tu.input.get("instruction", "")
|
||||
decision = await _request_browser_approval(
|
||||
session, tu.name, {"problem": problem, "instruction": instruction},
|
||||
)
|
||||
result_text = "User resolved the issue." if decision.get("behavior") != "deny" else "User declined to help."
|
||||
tool_results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tu.id,
|
||||
"content": [{"type": "text", "text": result_text}],
|
||||
})
|
||||
result_msg = Message(
|
||||
role="tool_result",
|
||||
content={"text": result_text, "tool_name": tu.name, "elapsed_ms": 0},
|
||||
)
|
||||
session.messages.append(result_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": result_msg.model_dump(mode="json"),
|
||||
})
|
||||
continue
|
||||
|
||||
policy = _browser_perms.get(tu.name, "always_allow")
|
||||
|
||||
if policy == "deny":
|
||||
|
||||
@@ -127,44 +127,72 @@ async def oauth_callback(code: str = Query(...), state: str = Query("")):
|
||||
return HTMLResponse("<html><body><h2>Invalid OAuth state</h2></body></html>", status_code=400)
|
||||
|
||||
tool = _load(tool_id)
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET", "")
|
||||
_port = os.environ.get("OPENSWARM_PORT", "8324")
|
||||
redirect_uri = f"http://localhost:{_port}/api/tools/oauth/callback"
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(GOOGLE_TOKEN_URL, data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
})
|
||||
if tool.name.lower() == "notion":
|
||||
# Notion OAuth: Basic auth with client_id:secret
|
||||
notion_client_id = os.environ.get("NOTION_OAUTH_CLIENT_ID", "")
|
||||
notion_client_secret = os.environ.get("NOTION_OAUTH_CLIENT_SECRET", "")
|
||||
import base64
|
||||
credentials = base64.b64encode(f"{notion_client_id}:{notion_client_secret}".encode()).decode()
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post("https://api.notion.com/v1/oauth/token", json={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
}, headers={
|
||||
"Authorization": f"Basic {credentials}",
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"OAuth token exchange failed: {resp.text}")
|
||||
return HTMLResponse(f"<html><body><h2>Token exchange failed</h2><pre>{resp.text}</pre></body></html>", status_code=400)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"Notion OAuth token exchange failed: {resp.text}")
|
||||
return HTMLResponse(f"<html><body><h2>Token exchange failed</h2><pre>{resp.text}</pre></body></html>", status_code=400)
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token", "")
|
||||
tool.oauth_tokens = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": tokens.get("refresh_token", ""),
|
||||
"token_expiry": time.time() + tokens.get("expires_in", 3600),
|
||||
}
|
||||
tool.auth_status = "connected"
|
||||
tokens = resp.json()
|
||||
tool.oauth_tokens = {
|
||||
"access_token": tokens.get("access_token", ""),
|
||||
}
|
||||
tool.auth_status = "connected"
|
||||
tool.connected_account_email = tokens.get("workspace_name", "Notion workspace")
|
||||
else:
|
||||
# Google OAuth
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET", "")
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(GOOGLE_TOKEN_URL, data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
})
|
||||
|
||||
if access_token:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as info_client:
|
||||
info_resp = await info_client.get(
|
||||
GOOGLE_USERINFO_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if info_resp.status_code == 200:
|
||||
tool.connected_account_email = info_resp.json().get("email")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch Google userinfo: {e}")
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"OAuth token exchange failed: {resp.text}")
|
||||
return HTMLResponse(f"<html><body><h2>Token exchange failed</h2><pre>{resp.text}</pre></body></html>", status_code=400)
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token", "")
|
||||
tool.oauth_tokens = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": tokens.get("refresh_token", ""),
|
||||
"token_expiry": time.time() + tokens.get("expires_in", 3600),
|
||||
}
|
||||
tool.auth_status = "connected"
|
||||
|
||||
if access_token:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as info_client:
|
||||
info_resp = await info_client.get(
|
||||
GOOGLE_USERINFO_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if info_resp.status_code == 200:
|
||||
tool.connected_account_email = info_resp.json().get("email")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch Google userinfo: {e}")
|
||||
|
||||
_save(tool)
|
||||
|
||||
@@ -391,6 +419,9 @@ 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"):
|
||||
env["GOOGLE_WORKSPACE_REFRESH_TOKEN"] = tool.oauth_tokens["refresh_token"]
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
@@ -407,13 +438,29 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
|
||||
pkg_name = next((a for a in (config.get("args") or []) if not a.startswith("-")), None)
|
||||
if pkg_name:
|
||||
_backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
bundle_path = os.path.join(_backend, "mcp-bundles", f"{pkg_name}.js")
|
||||
electron_path = os.environ.get("OPENSWARM_ELECTRON_PATH")
|
||||
# Check for single-file bundle first (e.g. reddit-mcp-buddy)
|
||||
bundle_path = os.path.join(_backend, "mcp-bundles", f"{pkg_name}.js")
|
||||
if os.path.isfile(bundle_path) and electron_path:
|
||||
config["command"] = electron_path
|
||||
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
|
||||
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")
|
||||
if os.path.isfile(pkg_json_path):
|
||||
import json as _json
|
||||
with open(pkg_json_path) as f:
|
||||
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}")
|
||||
|
||||
if not os.path.isabs(config.get("command", "")):
|
||||
resolved = _resolve_command(config["command"])
|
||||
@@ -611,6 +658,7 @@ async def _discover_mcp_tools_stdio(command: str, args: list[str] | None = None,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
env=proc_env,
|
||||
limit=10 * 1024 * 1024, # 10 MB buffer for large tool lists
|
||||
)
|
||||
|
||||
async def _send(msg: dict) -> None:
|
||||
@@ -685,22 +733,26 @@ async def discover_tools(tool_id: str):
|
||||
tool = _load(tool_id)
|
||||
|
||||
if tool.auth_type == "oauth2" and tool.auth_status == "connected":
|
||||
refreshed = await refresh_google_token(tool)
|
||||
if not refreshed and tool.oauth_tokens.get("access_token"):
|
||||
expiry = tool.oauth_tokens.get("token_expiry", 0)
|
||||
if time.time() >= expiry - 60:
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
# Only attempt Google token refresh for tools that use Google OAuth
|
||||
# (identified by having a refresh_token — Notion and other non-Google
|
||||
# OAuth providers don't use refresh tokens through our flow).
|
||||
if tool.oauth_tokens.get("refresh_token"):
|
||||
refreshed = await refresh_google_token(tool)
|
||||
if not refreshed and tool.oauth_tokens.get("access_token"):
|
||||
expiry = tool.oauth_tokens.get("token_expiry", 0)
|
||||
if time.time() >= expiry - 60:
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="OAuth token expired and GOOGLE_OAUTH_CLIENT_ID is not set. "
|
||||
"In the packaged app, create ~/.openswarm.env or "
|
||||
"~/Library/Application Support/OpenSwarm/.env with your Google OAuth credentials.",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="OAuth token expired and GOOGLE_OAUTH_CLIENT_ID is not set. "
|
||||
"In the packaged app, create ~/.openswarm.env or "
|
||||
"~/Library/Application Support/OpenSwarm/.env with your Google OAuth credentials.",
|
||||
status_code=502,
|
||||
detail="OAuth token expired and refresh failed. Try reconnecting Google.",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="OAuth token expired and refresh failed. Try reconnecting Google.",
|
||||
)
|
||||
|
||||
config = derive_mcp_config(tool)
|
||||
if not config:
|
||||
@@ -798,27 +850,39 @@ async def oauth_disconnect(tool_id: str):
|
||||
|
||||
@tools_lib.router.post("/{tool_id}/oauth/start")
|
||||
async def oauth_start(tool_id: str):
|
||||
_load(tool_id)
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
raise HTTPException(status_code=400, detail="GOOGLE_OAUTH_CLIENT_ID not set in backend .env")
|
||||
|
||||
tool = _load(tool_id)
|
||||
_port = os.environ.get("OPENSWARM_PORT", "8324")
|
||||
redirect_uri = f"http://localhost:{_port}/api/tools/oauth/callback"
|
||||
state = tool_id
|
||||
|
||||
_pending_oauth[state] = tool_id
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": " ".join(GOOGLE_SCOPES),
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
"state": state,
|
||||
}
|
||||
auth_url = f"{GOOGLE_AUTH_URL}?{urlencode(params)}"
|
||||
if tool.name.lower() == "notion":
|
||||
client_id = os.environ.get("NOTION_OAUTH_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
raise HTTPException(status_code=400, detail="NOTION_OAUTH_CLIENT_ID not set in backend .env")
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"owner": "user",
|
||||
"state": state,
|
||||
}
|
||||
auth_url = f"https://api.notion.com/v1/oauth/authorize?{urlencode(params)}"
|
||||
else:
|
||||
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
raise HTTPException(status_code=400, detail="GOOGLE_OAUTH_CLIENT_ID not set in backend .env")
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": " ".join(GOOGLE_SCOPES),
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
"state": state,
|
||||
}
|
||||
auth_url = f"{GOOGLE_AUTH_URL}?{urlencode(params)}"
|
||||
|
||||
return {"auth_url": auth_url}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "notion-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": {
|
||||
"@notionhq/notion-mcp-server": "^2.2.1"
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,20 @@ const INTEGRATIONS: Integration[] = [
|
||||
),
|
||||
authType: 'oauth2',
|
||||
},
|
||||
{
|
||||
id: 'notion',
|
||||
name: 'Notion',
|
||||
description: 'Create, read, and update pages, databases, and blocks in Notion workspaces.',
|
||||
mcp_config: { type: 'stdio', command: 'npx', args: ['-y', '@notionhq/notion-mcp-server'] },
|
||||
color: '#000000',
|
||||
website: 'https://www.notion.so',
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" width="22" height="22">
|
||||
<path d="M4.46 2.95c.53.43.73.4 1.73.33l9.4-.57c.2 0 .03-.2-.03-.23l-1.57-1.13c-.3-.23-.7-.5-1.47-.43L3.3 1.78c-.5.07-.6.33-.4.53l1.56 1.13v.51zM5.03 6.15v9.93c0 .53.27.73.87.7l10.33-.6c.6-.03.67-.4.67-.83V5.52c0-.43-.17-.63-.53-.6l-10.8.63c-.4.03-.54.2-.54.6zM14.93 6.63c.07.3 0 .6-.3.63l-.5.1v7.33c-.43.23-.83.37-1.17.37-.53 0-.67-.17-1.07-.67l-3.27-5.13v4.97l1.03.23s0 .6-.83.6l-2.3.13c-.07-.13 0-.47.23-.53l.6-.17V8.33l-.83-.07c-.07-.3.1-.73.57-.77l2.47-.17 3.4 5.2V8.6l-.87-.1c-.07-.37.2-.63.53-.67l2.3-.2zM2.57 1.28L11.93.28c1.17-.1 1.47-.03 2.2.5l3.03 2.13c.5.37.67.47.67.87v15.27c0 .63-.23 1-.87 1.07l-10.7.63c-.5.03-.73-.07-.97-.37L2.73 17.3c-.27-.33-.4-.6-.4-1.03V2.15c0-.53.23-.83.83-.87h-.59z" fill="#000"/>
|
||||
</svg>
|
||||
),
|
||||
authType: 'oauth2',
|
||||
},
|
||||
];
|
||||
|
||||
const CATEGORY_ORDER = ['filesystem', 'system', 'search', 'interaction', 'agents', 'planning', 'scheduling'];
|
||||
@@ -773,11 +787,11 @@ const Tools: React.FC = () => {
|
||||
const afterConnect = async () => {
|
||||
const statusResult = await dispatch(fetchToolStatus(toolId));
|
||||
if (fetchToolStatus.fulfilled.match(statusResult) && statusResult.payload.auth_status === 'connected') {
|
||||
setSnackbar({ open: true, message: 'Google account connected! Discovering actions…' });
|
||||
setSnackbar({ open: true, message: 'Account connected! Discovering actions…' });
|
||||
setExpandedToolId(toolId);
|
||||
dispatch(discoverTools(toolId));
|
||||
} else {
|
||||
setSnackbar({ open: true, message: 'Google account connected!' });
|
||||
setSnackbar({ open: true, message: 'Account connected!' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1425,7 +1439,7 @@ const Tools: React.FC = () => {
|
||||
onClick={(e) => { e.stopPropagation(); handleOAuthConnect(tool.id); }}
|
||||
sx={{ borderColor: `${c.status.info}40`, color: c.status.info, '&:hover': { borderColor: c.status.info, bgcolor: `${c.status.info}10` }, textTransform: 'none', fontSize: '0.78rem', borderRadius: 1.5, py: 0.5, flexShrink: 0 }}
|
||||
>
|
||||
Connect Google
|
||||
Connect {tool.name}
|
||||
</Button>
|
||||
)}
|
||||
{!isDisabled && ig?.credentialFields && tool.auth_status !== 'connected' && (
|
||||
|
||||
+29
-3
@@ -70,15 +70,41 @@ echo ""
|
||||
MCP_BUNDLE_DIR="$PROJECT_ROOT/backend/mcp-bundles"
|
||||
mkdir -p "$MCP_BUNDLE_DIR"
|
||||
if [[ ! -f "$MCP_BUNDLE_DIR/reddit-mcp-buddy.js" ]]; then
|
||||
echo "[0b] Bundling npm MCP servers..."
|
||||
echo "[0b] Bundling reddit-mcp-buddy..."
|
||||
TMPDIR_MCP=$(mktemp -d)
|
||||
cd "$TMPDIR_MCP"
|
||||
npm install reddit-mcp-buddy --silent 2>/dev/null
|
||||
npx esbuild node_modules/reddit-mcp-buddy/dist/index.js --bundle --platform=node --format=cjs --outfile="$MCP_BUNDLE_DIR/reddit-mcp-buddy.js" 2>/dev/null
|
||||
rm -rf "$TMPDIR_MCP"
|
||||
echo "npm MCP servers bundled."
|
||||
echo "reddit-mcp-buddy bundled."
|
||||
else
|
||||
echo "[0b] npm MCP bundles already present."
|
||||
echo "[0b] reddit-mcp-buddy bundle already present."
|
||||
fi
|
||||
|
||||
# Step 0c: Pre-install npm MCP servers that can't be esbuild'd
|
||||
NPM_SERVERS_DIR="$PROJECT_ROOT/backend/npm-servers"
|
||||
mkdir -p "$NPM_SERVERS_DIR"
|
||||
|
||||
if [[ ! -d "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server/node_modules" ]]; then
|
||||
echo "[0c] Installing @softeria/ms-365-mcp-server..."
|
||||
mkdir -p "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server"
|
||||
cd "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server"
|
||||
npm init -y > /dev/null 2>&1
|
||||
npm install @softeria/ms-365-mcp-server --silent 2>/dev/null
|
||||
echo "@softeria/ms-365-mcp-server installed."
|
||||
else
|
||||
echo "[0c] @softeria/ms-365-mcp-server already present."
|
||||
fi
|
||||
|
||||
if [[ ! -d "$NPM_SERVERS_DIR/notionhq-notion-mcp-server/node_modules" ]]; then
|
||||
echo "[0c] Installing @notionhq/notion-mcp-server..."
|
||||
mkdir -p "$NPM_SERVERS_DIR/notionhq-notion-mcp-server"
|
||||
cd "$NPM_SERVERS_DIR/notionhq-notion-mcp-server"
|
||||
npm init -y > /dev/null 2>&1
|
||||
npm install @notionhq/notion-mcp-server --silent 2>/dev/null
|
||||
echo "@notionhq/notion-mcp-server installed."
|
||||
else
|
||||
echo "[0c] @notionhq/notion-mcp-server already present."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user