From b88effb404c8fe8d01dd282ea0e4be879ceaa0de Mon Sep 17 00:00:00 2001 From: Haoran Zhang Date: Fri, 31 Jul 2026 14:01:14 -0700 Subject: [PATCH 1/2] feat: add AdaL agent support (skills + MCP) Adds AdaL (https://github.com/SylphAI-Inc/adal) to the agent registries used by `firecrawl setup`/`firecrawl doctor`/`firecrawl init`, following the existing per-agent patterns (Hermes/OpenClaw-style direct config write, since AdaL has no `adal mcp add` subcommand). - utils/agents.ts: add `adal` AgentSpec for `firecrawl doctor` detection. Presence = ~/.adal; MCP config = ~/.adal/settings.json. The existing recursive JSON walker (hasFirecrawlMcpEntry) already finds a nested `mcpServers.firecrawl` entry at any depth, so no walker changes needed. - commands/skills-native.ts: add `adal` AgentConfig for native skill installs. Global skills dir = ~/.adal/skills (matches AdaL's SkillsManager personal-skills path). - commands/setup.ts: add `installAdalMcp()`, mirroring installHermesMcp(). AdaL stores MCP servers per-project inside a single global ~/.adal/settings.json, under `projects..mcpServers` (schema per deep_research/mcp_integration/mcp_client_manager.py MCPServerConfig). Registers against process.cwd() as the project path. Wired into `resolveMcpAgent`, `installMcp` dispatch, and `installAllMcpLaunchers` (--agent all). Tested E2E against a real AdaL checkout: - `firecrawl setup mcp --agent adal` correctly writes projects..mcpServers.firecrawl into ~/.adal/settings.json. - AdaL's MCP client manager discovers it and registers 3 live tools (mcp_firecrawl_firecrawl_scrape/search/parse) against Firecrawl's hosted MCP server (keyless). - AdaL successfully invoked mcp_firecrawl_firecrawl_scrape against https://example.com and returned real markdown/metadata from Firecrawl's API. - installSkillsNative({ agent: 'adal' }) correctly symlinks all 10 firecrawl/cli skills into ~/.adal/skills/, and AdaL's own SkillsManager (deep_research/skills/manager.py) successfully discovers and indexes all 10 as personal skills. All 391 existing tests pass; no changes to existing agent behavior. --- src/commands/setup.ts | 85 +++++++++++++++++++++++++++++++++++ src/commands/skills-native.ts | 9 ++++ src/utils/agents.ts | 14 +++++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index cd90403aa3..133123bab6 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -37,6 +37,7 @@ type ResolvedMcpAgent = | { kind: 'add-mcp'; agent?: string; all?: boolean } | { kind: 'hermes' } | { kind: 'openclaw' } + | { kind: 'adal' } | { kind: 'all-launchers' }; export interface SetupOptions { @@ -200,6 +201,7 @@ function environmentHeaderForAgent(agent?: string): string | undefined { case 'claude-code': case 'hermes': case 'openclaw': + case 'adal': return `Bearer \${${ENV_API_KEY}}`; case 'cursor': case 'vscode': @@ -258,6 +260,8 @@ function resolveMcpAgent(agent: string | undefined): ResolvedMcpAgent { return { kind: 'hermes' }; case 'openclaw': return { kind: 'openclaw' }; + case 'adal': + return { kind: 'adal' }; default: return { kind: 'add-mcp', agent }; } @@ -550,6 +554,10 @@ export async function installMcp(options: SetupOptions): Promise { await installOpenClawMcp(); return; } + if (resolvedAgent.kind === 'adal') { + await installAdalMcp(options); + return; + } if (resolvedAgent.kind === 'all-launchers') { await installAllMcpLaunchers(options); return; @@ -564,6 +572,7 @@ async function installAllMcpLaunchers(options: SetupOptions): Promise { } await installHermesMcp(); await installOpenClawMcp(); + await installAdalMcp(options); } async function installAddMcp( @@ -704,6 +713,82 @@ export async function installHermesMcp(): Promise { console.log(`Hermes Agent MCP configured at ${configPath}.`); } +/** + * Install the Firecrawl MCP server into AdaL. + * + * AdaL stores MCP servers per-project inside a single global + * `~/.adal/settings.json`, under `projects..mcpServers` + * (see deep_research/src/deep_research/mcp_integration/mcp_client_manager.py + * `MCPServerConfig` for the schema AdaL itself reads/writes). We register + * against the current working directory as the project path, matching how + * AdaL scopes MCP servers to the project you're running it from. + */ +export async function installAdalMcp(options: SetupOptions = {}): Promise { + const config = firecrawlMcpConfig('adal'); + const configPath = path.join(os.homedir(), '.adal', 'settings.json'); + const projectPath = process.cwd(); + + mkdirSync(path.dirname(configPath), { recursive: true }); + + const existing = existsSync(configPath) + ? readFileSync(configPath, 'utf-8') + : ''; + let root: Record; + try { + root = existing ? JSON.parse(existing) : {}; + } catch { + throw new Error( + `Failed to parse existing AdaL settings at ${configPath}. Fix or remove the file, then retry.` + ); + } + + const projects = + typeof root.projects === 'object' && + root.projects !== null && + !Array.isArray(root.projects) + ? (root.projects as Record) + : {}; + + const project = + typeof projects[projectPath] === 'object' && + projects[projectPath] !== null && + !Array.isArray(projects[projectPath]) + ? (projects[projectPath] as Record) + : {}; + + const mcpServers = + typeof project.mcpServers === 'object' && + project.mcpServers !== null && + !Array.isArray(project.mcpServers) + ? (project.mcpServers as Record) + : {}; + + mcpServers.firecrawl = { + name: 'firecrawl', + type: 'http', + url: config.url, + headers: config.headers, + enabled: true, + trust: false, + }; + project.mcpServers = mcpServers; + projects[projectPath] = project; + root.projects = projects; + + writeFileSync(configPath, JSON.stringify(root, null, 2) + '\n', { + encoding: 'utf-8', + mode: 0o600, + }); + if (process.platform !== 'win32') { + chmodSync(configPath, 0o600); + } + if (!options.quiet) { + console.log( + `AdaL MCP configured at ${configPath} for project ${projectPath}.` + ); + } +} + export async function installOpenClawMcp(): Promise { const config = { ...firecrawlMcpConfig('openclaw'), diff --git a/src/commands/skills-native.ts b/src/commands/skills-native.ts index c899d17e02..8483ac9032 100644 --- a/src/commands/skills-native.ts +++ b/src/commands/skills-native.ts @@ -120,6 +120,15 @@ const AGENTS: AgentConfig[] = [ globalSkillsDir: '.hermes/skills', detectDir: '.hermes', }, + { + // AdaL's SkillsManager loads personal skills from `~/.adal/skills/` + // (see deep_research/src/deep_research/main_agent.py `_load_skills_index`) + // and project skills from `/.adal/skills/`. Global install target + // matches the personal-skills path. + name: 'adal', + globalSkillsDir: '.adal/skills', + detectDir: '.adal', + }, ]; /** Canonical directory for skill files — single source of truth */ diff --git a/src/utils/agents.ts b/src/utils/agents.ts index 0ba6ea4aec..92685df9b5 100644 --- a/src/utils/agents.ts +++ b/src/utils/agents.ts @@ -18,7 +18,8 @@ export type AgentId = | 'vscode' | 'windsurf' | 'codex' - | 'continue'; + | 'continue' + | 'adal'; export interface AgentDetection { id: AgentId; @@ -124,6 +125,17 @@ const SPECS: AgentSpec[] = [ path.join(cwd, '.continue', 'config.json'), ], }, + { + id: 'adal', + name: 'AdaL', + presencePaths: () => [path.join(home, '.adal')], + // AdaL stores MCP servers per-project under + // `projects..mcpServers` inside a single global + // settings.json. The generic JSON walker below (hasFirecrawlMcpEntry) + // recurses through nested objects, so pointing at settings.json is + // enough — no project-path resolution needed for detection. + mcpConfigPaths: () => [path.join(home, '.adal', 'settings.json')], + }, ]; async function pathExists(p: string): Promise { From d83dc45904b84e802c46d690eaa82716e3ce9265 Mon Sep 17 00:00:00 2001 From: Haoran Zhang Date: Fri, 31 Jul 2026 14:28:39 -0700 Subject: [PATCH 2/2] fix: respect --keyless and validate settings.json shape in installAdalMcp Addresses cubic review feedback on PR #176: 1. installAdalMcp() was calling firecrawlMcpConfig('adal'), which unconditionally calls getApiKey() regardless of options.keyless. `firecrawl setup mcp --agent adal --keyless` would still write an Authorization header if a Firecrawl API key happened to be stored. Fixed by resolving apiKey the same way installAddMcp does (`options.keyless ? undefined : getApiKey()`) and passing it through firecrawlMcpHeaders('adal', apiKey) directly. 2. JSON.parse() succeeds on any valid JSON value, not just objects (e.g. `null`, `[1,2,3]`, `"a string"`). The previous code assumed the parsed result was always a plain object and read/wrote `root.projects` on it directly, which throws on null and silently drops the write on arrays/primitives (JSON.stringify ignores non-index properties on arrays). Added an explicit object-shape check after parsing, reusing the same actionable error message as the JSON.parse() failure case. Verified both fixes directly against the compiled dist build: - installAdalMcp({ keyless: true }) with a stored API key present now writes a firecrawl entry with no `headers` field. - installAdalMcp() against a `~/.adal/settings.json` containing `[1,2,3]` now throws the actionable "Failed to parse existing AdaL settings..." error instead of crashing on `null`/array property access or silently discarding the registration. All 391 existing tests still pass (44/44 in setup.test.ts). --- src/commands/setup.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 133123bab6..93d1300606 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -724,7 +724,11 @@ export async function installHermesMcp(): Promise { * AdaL scopes MCP servers to the project you're running it from. */ export async function installAdalMcp(options: SetupOptions = {}): Promise { - const config = firecrawlMcpConfig('adal'); + const apiKey = options.keyless ? undefined : getApiKey(); + const config: { url: string; headers?: Record } = { + url: firecrawlHostedMcpUrl(), + headers: firecrawlMcpHeaders('adal', apiKey), + }; const configPath = path.join(os.homedir(), '.adal', 'settings.json'); const projectPath = process.cwd(); @@ -733,14 +737,21 @@ export async function installAdalMcp(options: SetupOptions = {}): Promise const existing = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : ''; - let root: Record; + const settingsFileError = `Failed to parse existing AdaL settings at ${configPath}. Fix or remove the file, then retry.`; + let parsed: unknown; try { - root = existing ? JSON.parse(existing) : {}; + parsed = existing ? JSON.parse(existing) : {}; } catch { - throw new Error( - `Failed to parse existing AdaL settings at ${configPath}. Fix or remove the file, then retry.` - ); + throw new Error(settingsFileError); + } + if ( + typeof parsed !== 'object' || + parsed === null || + Array.isArray(parsed) + ) { + throw new Error(settingsFileError); } + const root = parsed as Record; const projects = typeof root.projects === 'object' &&