crw-mcp 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agents.js CHANGED
@@ -53,21 +53,51 @@ function installSkill(agent, skillContent) {
53
53
  const dir = path.join(home, agent.configDir, "skills", "crw");
54
54
  fs.mkdirSync(dir, { recursive: true });
55
55
  const file = path.join(dir, "SKILL.md");
56
- fs.writeFileSync(file, skillContent, "utf-8");
56
+ atomicWriteText(file, skillContent);
57
57
  return file;
58
58
  }
59
59
 
60
60
  function readJson(file) {
61
+ if (!fs.existsSync(file)) return {};
61
62
  try {
62
- return JSON.parse(fs.readFileSync(file, "utf-8"));
63
- } catch {
64
- return {};
63
+ const value = JSON.parse(fs.readFileSync(file, "utf-8"));
64
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
65
+ throw new Error("top-level value must be an object");
66
+ }
67
+ return value;
68
+ } catch (error) {
69
+ throw new Error(`refusing to overwrite malformed JSON in ${file}: ${error.message}`);
65
70
  }
66
71
  }
67
72
 
68
- function writeJson(file, obj) {
73
+ function ensureObject(value, label, file) {
74
+ if (value === undefined) return {};
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76
+ throw new Error(`refusing to replace non-object ${label} in ${file}`);
77
+ }
78
+ return value;
79
+ }
80
+
81
+ function backupOnce(file) {
82
+ if (!fs.existsSync(file)) return;
83
+ const backup = `${file}.crw-backup`;
84
+ if (!fs.existsSync(backup)) fs.copyFileSync(file, backup);
85
+ }
86
+
87
+ function atomicWriteText(file, content) {
69
88
  fs.mkdirSync(path.dirname(file), { recursive: true });
70
- fs.writeFileSync(file, JSON.stringify(obj, null, 2) + "\n", "utf-8");
89
+ backupOnce(file);
90
+ const tmp = `${file}.crw-tmp-${process.pid}-${Date.now()}`;
91
+ try {
92
+ fs.writeFileSync(tmp, content, { encoding: "utf-8", mode: 0o600 });
93
+ fs.renameSync(tmp, file);
94
+ } finally {
95
+ try { fs.unlinkSync(tmp); } catch { /* already renamed or never created */ }
96
+ }
97
+ }
98
+
99
+ function writeJson(file, obj) {
100
+ atomicWriteText(file, JSON.stringify(obj, null, 2) + "\n");
71
101
  }
72
102
 
73
103
  /**
@@ -90,8 +120,15 @@ function installMcp(agent, env) {
90
120
 
91
121
  if (m.kind === "claude-cli") {
92
122
  // add-json avoids the `-e` variadic-name pitfall and merges ~/.claude.json
93
- // (user scope) correctly. Idempotent-ish: remove a stale entry first.
123
+ // (user scope) correctly. Probe before touching state, then restore the
124
+ // exact prior user config if replacement fails.
94
125
  const server = { command: MCP_CMD, args: MCP_ARGS, ...(hasEnv ? { env } : {}) };
126
+ const probe = spawnSync("claude", ["--version"], { stdio: "ignore" });
127
+ if (probe.error || probe.status !== 0) {
128
+ throw new Error("Claude Code CLI was detected but is not launchable");
129
+ }
130
+ const configFile = path.join(home, ".claude.json");
131
+ const original = fs.existsSync(configFile) ? fs.readFileSync(configFile) : null;
95
132
  spawnSync("claude", ["mcp", "remove", "--scope", "user", "crw"], { stdio: "ignore" });
96
133
  const r = spawnSync(
97
134
  "claude",
@@ -99,7 +136,12 @@ function installMcp(agent, env) {
99
136
  { stdio: "ignore" },
100
137
  );
101
138
  if (r.error || r.status !== 0) {
102
- return "MCP: run `claude mcp add-json --scope user crw '" + JSON.stringify(server) + "'` (claude CLI not found)";
139
+ if (original) {
140
+ fs.writeFileSync(configFile, original);
141
+ } else {
142
+ try { fs.unlinkSync(configFile); } catch { /* no file was created */ }
143
+ }
144
+ throw new Error("Claude Code rejected the MCP registration; previous config restored");
103
145
  }
104
146
  return "MCP via claude CLI (user scope)";
105
147
  }
@@ -107,7 +149,7 @@ function installMcp(agent, env) {
107
149
  if (m.kind === "json") {
108
150
  const file = path.join(home, m.file);
109
151
  const cfg = readJson(file);
110
- cfg.mcpServers = cfg.mcpServers || {};
152
+ cfg.mcpServers = ensureObject(cfg.mcpServers, "mcpServers", file);
111
153
  cfg.mcpServers.crw = {
112
154
  ...(m.withType ? { type: "stdio" } : {}),
113
155
  command: MCP_CMD,
@@ -122,7 +164,7 @@ function installMcp(agent, env) {
122
164
  const file = path.join(home, m.file);
123
165
  const cfg = readJson(file);
124
166
  if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
125
- cfg.mcp = cfg.mcp || {};
167
+ cfg.mcp = ensureObject(cfg.mcp, "mcp", file);
126
168
  cfg.mcp.crw = {
127
169
  type: "local",
128
170
  command: [MCP_CMD, ...MCP_ARGS],
@@ -140,20 +182,38 @@ function installMcp(agent, env) {
140
182
  } catch {
141
183
  /* new file */
142
184
  }
143
- if (/\[mcp_servers\.crw\]/.test(toml)) {
144
- return `MCP already in ${m.file} (left as-is)`;
145
- }
185
+ // The installer owns the crw section. Replace it so a previous install
186
+ // with embedded env credentials cannot override a later `--from-config`
187
+ // setup (and so re-runs remain idempotent).
188
+ toml = removeOwnedCodexSections(toml);
146
189
  const envLines = hasEnv
147
190
  ? "\n[mcp_servers.crw.env]\n" +
148
191
  Object.entries(env).map(([k, v]) => `${k} = "${v}"`).join("\n") + "\n"
149
192
  : "";
150
193
  const block = `\n[mcp_servers.crw]\ncommand = "${MCP_CMD}"\nargs = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]\n${envLines}`;
151
- fs.mkdirSync(path.dirname(file), { recursive: true });
152
- fs.appendFileSync(file, (toml && !toml.endsWith("\n") ? "\n" : "") + block, "utf-8");
194
+ const next = toml + (toml && !toml.endsWith("\n") ? "\n" : "") + block;
195
+ atomicWriteText(file, next);
153
196
  return `MCP → ${m.file}`;
154
197
  }
155
198
 
156
199
  return "MCP: unsupported";
157
200
  }
158
201
 
202
+ function removeOwnedCodexSections(toml) {
203
+ const lines = toml.split("\n");
204
+ const kept = [];
205
+ let skipping = false;
206
+ for (const line of lines) {
207
+ const isSection = /^\s*\[[^\]]+\]/.test(line);
208
+ const isOwned = /^\s*\[mcp_servers\.crw(?:\.[^\]]+)?\]\s*(?:#.*)?$/.test(line);
209
+ if (isOwned) {
210
+ skipping = true;
211
+ continue;
212
+ }
213
+ if (isSection) skipping = false;
214
+ if (!skipping) kept.push(line);
215
+ }
216
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
217
+ }
218
+
159
219
  module.exports = { AGENTS, detectAgents, getApiKey, readSkill, installSkill, installMcp, home };
package/bin/init.js CHANGED
@@ -73,6 +73,6 @@ if (apiKey) {
73
73
  console.log(" export CRW_API_URL=https://api.fastcrw.com");
74
74
  } else {
75
75
  console.log("\nThe skill works in local mode out of the box (free, embedded).");
76
- console.log("Cloud mode (managed, 500 free credits): https://fastcrw.com");
76
+ console.log("Cloud mode (managed, 1000 free credits): https://fastcrw.com");
77
77
  }
78
78
  console.log("\nDocs: https://fastcrw.com/docs");
package/bin/install.js CHANGED
@@ -36,6 +36,7 @@ Options:
36
36
  --gemini-cli Gemini CLI --windsurf Windsurf
37
37
  --api-key <key> fastcrw.com API key → cloud mode (else local/embedded)
38
38
  --api-url <url> API base (default https://api.fastcrw.com when a key is set)
39
+ --from-config Read mode from ~/.config/crw/config.toml at runtime
39
40
  -h, --help Show this help
40
41
 
41
42
  Without flags, auto-detects installed agents. Installs the skill (SKILL.md) and
@@ -44,7 +45,8 @@ registers the "crw" MCP server. Run \`crw-mcp init\` for the skill only.
44
45
  process.exit(0);
45
46
  }
46
47
 
47
- const apiKey = getApiKey(args);
48
+ const fromConfig = args.includes("--from-config");
49
+ const apiKey = fromConfig ? null : getApiKey(args);
48
50
  const apiUrl = argValue("--api-url") || (apiKey ? "https://api.fastcrw.com" : null);
49
51
  const env = apiKey ? { CRW_API_KEY: apiKey, CRW_API_URL: apiUrl } : {};
50
52
 
@@ -58,6 +60,7 @@ if (agents.length === 0) {
58
60
 
59
61
  const skill = readSkill();
60
62
  const results = [];
63
+ const failures = [];
61
64
  for (const agent of agents) {
62
65
  try {
63
66
  const skillPath = installSkill(agent, skill);
@@ -65,6 +68,7 @@ for (const agent of agents) {
65
68
  results.push({ name: agent.name, skillPath, mcpLabel });
66
69
  } catch (err) {
67
70
  console.error(` ${agent.name}: ${err.message}`);
71
+ failures.push(agent.name);
68
72
  }
69
73
  }
70
74
 
@@ -80,11 +84,17 @@ for (const r of results) {
80
84
  console.log(` ${r.mcpLabel.replace(home, "~")}`);
81
85
  }
82
86
 
83
- if (apiKey) {
87
+ if (fromConfig) {
88
+ console.log("\nMode: follows ~/.config/crw/config.toml (credentials are not copied into agent configs).");
89
+ } else if (apiKey) {
84
90
  console.log(`\nMode: cloud — CRW_API_KEY ${apiKey.slice(0, 10)}… → ${apiUrl}`);
85
91
  } else {
86
92
  console.log("\nMode: local (free, embedded binary — no key needed).");
87
- console.log(" Cloud mode: re-run with --api-key crw_live_… (500 free credits at https://fastcrw.com).");
93
+ console.log(" Cloud mode: re-run with --api-key crw_live_… (1000 free credits at https://fastcrw.com).");
88
94
  }
89
95
  console.log("\nRestart your agent to load the MCP server.");
90
96
  console.log("Docs: https://fastcrw.com/docs");
97
+ if (failures.length > 0) {
98
+ console.error(`\nFailed targets: ${failures.join(", ")}. Existing configs were preserved.`);
99
+ process.exitCode = 1;
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crw-mcp",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "MCP server for CRW web scraper — scrape, crawl, map, extract, search, and PDF-parse tools for AI agents",
5
5
  "license": "AGPL-3.0",
6
6
  "author": "fastCRW <hello@fastcrw.com>",
@@ -38,9 +38,9 @@
38
38
  "skills/SKILL.md"
39
39
  ],
40
40
  "optionalDependencies": {
41
- "crw-mcp-darwin-x64": "0.30.0",
42
- "crw-mcp-darwin-arm64": "0.30.0",
43
- "crw-mcp-linux-x64": "0.30.0",
44
- "crw-mcp-linux-arm64": "0.30.0"
41
+ "crw-mcp-darwin-x64": "0.31.0",
42
+ "crw-mcp-darwin-arm64": "0.31.0",
43
+ "crw-mcp-linux-x64": "0.31.0",
44
+ "crw-mcp-linux-arm64": "0.31.0"
45
45
  }
46
46
  }
package/skills/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: crw
3
- description: "Scrape, crawl, map, and search the web using fastCRW's native /v1 API. Use when the user needs web page content, site-wide extraction, URL discovery, or web search results. Single binary, 6 MB RAM; /v2 exists separately for Firecrawl migration."
3
+ description: "Scrape, crawl, map, and search the web using fastCRW's native /v1 API. Use when the user needs web page content, site-wide extraction, URL discovery, or web search results. Single binary, 14 MB RAM; /v2 exists separately for Firecrawl migration."
4
4
  license: AGPL-3.0
5
5
  metadata:
6
6
  author: us
@@ -32,8 +32,8 @@ npx crw-mcp@latest init # skill only
32
32
 
33
33
  ## Authentication
34
34
 
35
- - **Embedded mode** (default): No key needed — the MCP server runs a self-contained scraper in ~6 MB RAM. No server required.
36
- - **Cloud mode** (fastcrw.com): Set `CRW_API_KEY=crw_live_...` and `CRW_API_URL=https://api.fastcrw.com`. Get a free key at https://fastcrw.com with 500 one-time lifetime credits (never resets, not monthly).
35
+ - **Embedded mode** (default): No key needed — the MCP server runs a self-contained scraper in ~14 MB RAM. No server required.
36
+ - **Cloud mode** (fastcrw.com): Set `CRW_API_KEY=crw_live_...` and `CRW_API_URL=https://api.fastcrw.com`. Get a free key at https://fastcrw.com with 1000 one-time lifetime credits (never resets, not monthly).
37
37
 
38
38
  ## MCP Tools
39
39
 
@@ -194,7 +194,7 @@ Available fields: `title`, `url`, `description`, `snippet`, `position`,
194
194
 
195
195
  ## Links
196
196
 
197
- - Cloud API: https://fastcrw.com — 500 one-time lifetime free credits (never resets, not monthly)
197
+ - Cloud API: https://fastcrw.com — 1000 one-time lifetime free credits (never resets, not monthly)
198
198
  - Docs: https://docs.fastcrw.com
199
199
  - GitHub: https://github.com/us/crw
200
200
  - Native API: `/v1/scrape`, `/v1/crawl`, `/v1/map`, and `/v1/search` are the recommended routes for new CRW integrations