clawdeals-mcp 0.1.3

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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # clawdeals-mcp
2
+
3
+ Minimal **MCP stdio server** exposing the Clawdeals v0 tool catalog and forwarding each tool call 1:1 to the Clawdeals REST API.
4
+
5
+ If `npx clawdeals-mcp ...` returns npm `E404`, the package is not yet published in the registry you are using.
6
+ Use the repo-local commands instead:
7
+
8
+ ```bash
9
+ npm run mcp:stdio
10
+ npm run mcp:install
11
+ ```
12
+
13
+ ## Run
14
+
15
+ ```bash
16
+ export CLAWDEALS_API_KEY="cd_live_..."
17
+ export CLAWDEALS_API_BASE="https://app.clawdeals.com/api"
18
+
19
+ npx clawdeals-mcp
20
+ ```
21
+
22
+ ## Install into your IDE (recommended)
23
+
24
+ This updates supported MCP config files on your machine (Cursor and Claude Desktop).
25
+
26
+ ```bash
27
+ export CLAWDEALS_API_KEY="cd_live_..."
28
+ export CLAWDEALS_API_BASE="https://app.clawdeals.com/api"
29
+
30
+ npx clawdeals-mcp install
31
+ ```
32
+
33
+ Target a specific config file:
34
+
35
+ ```bash
36
+ npx clawdeals-mcp install -- --file "/path/to/mcp.json"
37
+ ```
38
+
39
+ ## Env
40
+
41
+ - `CLAWDEALS_API_KEY` (required)
42
+ - `CLAWDEALS_API_BASE` (required, example: `https://app.clawdeals.com/api`)
43
+ - `CLAWDEALS_ORIGIN` (optional, default: `mcp`)
44
+ - `CLAWDEALS_TIMEOUT_MS` (optional, default: `15000`)
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const pkgRoot = path.resolve(__dirname, "..");
10
+
11
+ const argv = process.argv.slice(2);
12
+ const cmd = argv[0];
13
+
14
+ function printHelp() {
15
+ process.stdout.write(`clawdeals-mcp (stdio MCP server)\n\nUsage:\n clawdeals-mcp Run the MCP stdio server\n clawdeals-mcp install [args] Install/update MCP client config (Cursor/Claude Desktop)\n\nEnv:\n CLAWDEALS_API_KEY Required\n CLAWDEALS_API_BASE Required (example: https://app.clawdeals.com/api)\n CLAWDEALS_ORIGIN Optional (default: mcp)\n CLAWDEALS_TIMEOUT_MS Optional (default: 15000)\n\nExamples:\n npx clawdeals-mcp\n npx clawdeals-mcp install\n npx clawdeals-mcp install -- --file /path/to/mcp.json\n`);
16
+ }
17
+
18
+ function printVersion() {
19
+ // Keep in sync with package.json; used primarily for debugging.
20
+ process.stdout.write("0.1.3\n");
21
+ }
22
+
23
+ if (cmd === "-h" || cmd === "--help") {
24
+ printHelp();
25
+ process.exit(0);
26
+ }
27
+
28
+ if (cmd === "-v" || cmd === "--version") {
29
+ printVersion();
30
+ process.exit(0);
31
+ }
32
+
33
+ function runNode(scriptRelPath, args) {
34
+ const scriptAbsPath = path.resolve(pkgRoot, scriptRelPath);
35
+ const result = spawnSync(process.execPath, [scriptAbsPath, ...args], {
36
+ stdio: "inherit",
37
+ env: process.env
38
+ });
39
+
40
+ if (result.error) {
41
+ process.stderr.write(`${String(result.error?.stack || result.error)}\n`);
42
+ process.exit(1);
43
+ }
44
+
45
+ process.exit(typeof result.status === "number" ? result.status : 1);
46
+ }
47
+
48
+ if (cmd === "install") {
49
+ // Forward remaining args to the installer.
50
+ // Note: allow "--" passthrough from npm/npx callers.
51
+ const passthrough = argv.slice(1).filter((a) => a !== "--");
52
+ runNode("mcp/install.mjs", passthrough);
53
+ }
54
+
55
+ if (cmd && cmd !== "stdio") {
56
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
57
+ printHelp();
58
+ process.exit(1);
59
+ }
60
+
61
+ // Default: run the stdio server.
62
+ runNode("mcp-server.mjs", cmd === "stdio" ? argv.slice(1) : argv);
@@ -0,0 +1,200 @@
1
+ import crypto from "node:crypto";
2
+ const DEFAULT_ORIGIN = "mcp";
3
+ const DEFAULT_TIMEOUT_MS = 15000;
4
+
5
+ function buildStableError({ code, message, details = {}, requestId, retryAfterSeconds = null }) {
6
+ return {
7
+ ok: false,
8
+ error: {
9
+ code: code || "ERROR",
10
+ message: message || "Request failed",
11
+ details: details && typeof details === "object" ? details : {}
12
+ },
13
+ meta: {
14
+ request_id: requestId || crypto.randomUUID(),
15
+ ...(typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) ? { retry_after_seconds: retryAfterSeconds } : {})
16
+ }
17
+ };
18
+ }
19
+
20
+ function codeFromStatus(status) {
21
+ if (status === 401) return "UNAUTHORIZED";
22
+ if (status === 403) return "FORBIDDEN";
23
+ if (status === 404) return "NOT_FOUND";
24
+ if (status === 409) return "CONFLICT";
25
+ if (status === 429) return "RATE_LIMITED";
26
+ return "ERROR";
27
+ }
28
+
29
+ function parseRetryAfterSeconds(value) {
30
+ if (!value) return null;
31
+ const n = Number.parseInt(String(value), 10);
32
+ if (!Number.isFinite(n) || n < 0) return null;
33
+ return n;
34
+ }
35
+
36
+ function normalizeBaseUrl(baseUrl) {
37
+ const raw = typeof baseUrl === "string" ? baseUrl.trim() : "";
38
+ if (!raw) return "";
39
+ return raw.replace(/\/+$/, "");
40
+ }
41
+
42
+ function normalizeTimeoutMs(value) {
43
+ const parsed = Number.parseInt(String(value || ""), 10);
44
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
45
+ return DEFAULT_TIMEOUT_MS;
46
+ }
47
+
48
+ function isWriteMethod(method) {
49
+ const normalized = String(method || "GET").toUpperCase();
50
+ return normalized !== "GET" && normalized !== "HEAD" && normalized !== "OPTIONS";
51
+ }
52
+
53
+ function safeJsonParse(text) {
54
+ if (!text) return { ok: true, value: {} };
55
+ try {
56
+ return { ok: true, value: JSON.parse(text) };
57
+ } catch (error) {
58
+ return { ok: false, value: null };
59
+ }
60
+ }
61
+
62
+ function toQueryStringParams(query) {
63
+ const params = new URLSearchParams();
64
+ if (!query || typeof query !== "object") return params;
65
+ for (const [key, value] of Object.entries(query)) {
66
+ if (value === undefined || value === null) continue;
67
+ if (Array.isArray(value)) {
68
+ // REST handlers in this repo typically expect comma-separated strings, not repeated keys.
69
+ params.set(key, value.map((v) => String(v)).join(","));
70
+ continue;
71
+ }
72
+ params.set(key, String(value));
73
+ }
74
+ return params;
75
+ }
76
+
77
+ export async function callClawdeals({
78
+ method,
79
+ path,
80
+ query,
81
+ body,
82
+ idempotencyKey,
83
+ requestId,
84
+ env = process.env,
85
+ fetchImpl = fetch
86
+ }) {
87
+ const apiKey = env.CLAWDEALS_API_KEY;
88
+ if (!apiKey) {
89
+ return buildStableError({
90
+ code: "UNAUTHORIZED",
91
+ message: "CLAWDEALS_API_KEY is required",
92
+ requestId
93
+ });
94
+ }
95
+
96
+ const baseUrl = normalizeBaseUrl(env.CLAWDEALS_API_BASE);
97
+ if (!baseUrl) {
98
+ return buildStableError({
99
+ code: "CONFIG_ERROR",
100
+ message: "CLAWDEALS_API_BASE is required (example: https://app.clawdeals.com/api)",
101
+ requestId
102
+ });
103
+ }
104
+ const origin = (env.CLAWDEALS_ORIGIN || DEFAULT_ORIGIN).trim() || DEFAULT_ORIGIN;
105
+ const timeoutMs = normalizeTimeoutMs(env.CLAWDEALS_TIMEOUT_MS);
106
+ const resolvedRequestId = requestId || crypto.randomUUID();
107
+
108
+ const url = new URL(`${baseUrl}${path}`);
109
+ const params = toQueryStringParams(query);
110
+ params.forEach((value, key) => url.searchParams.set(key, value));
111
+
112
+ const headers = {
113
+ accept: "application/json",
114
+ authorization: `Bearer ${apiKey}`,
115
+ "x-clawdeals-origin": origin,
116
+ "x-request-id": resolvedRequestId
117
+ };
118
+
119
+ if (idempotencyKey) {
120
+ headers["Idempotency-Key"] = String(idempotencyKey);
121
+ }
122
+
123
+ const controller = new AbortController();
124
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
125
+ // Prevent this timer from keeping the process alive.
126
+ timeout.unref?.();
127
+
128
+ const isWrite = isWriteMethod(method);
129
+ const shouldSendBody = isWrite && body !== undefined;
130
+
131
+ try {
132
+ const response = await fetchImpl(url.toString(), {
133
+ method,
134
+ headers: {
135
+ ...headers,
136
+ ...(shouldSendBody ? { "content-type": "application/json; charset=utf-8" } : {})
137
+ },
138
+ body: shouldSendBody ? JSON.stringify(body ?? {}) : undefined,
139
+ signal: controller.signal
140
+ });
141
+
142
+ clearTimeout(timeout);
143
+
144
+ const retryAfterSeconds = parseRetryAfterSeconds(response.headers.get("retry-after"));
145
+ const text = await response.text();
146
+ const parsed = safeJsonParse(text);
147
+
148
+ if (!parsed.ok) {
149
+ return buildStableError({
150
+ code: response.status >= 400 ? codeFromStatus(response.status) : "ERROR",
151
+ message: "Non-JSON response from server",
152
+ requestId: resolvedRequestId,
153
+ retryAfterSeconds
154
+ });
155
+ }
156
+
157
+ const value = parsed.value;
158
+
159
+ if (response.status < 400) {
160
+ return {
161
+ ok: true,
162
+ data: value,
163
+ meta: {
164
+ request_id: resolvedRequestId
165
+ }
166
+ };
167
+ }
168
+
169
+ const serverError = value && typeof value === "object" ? value.error : null;
170
+ if (serverError && typeof serverError === "object") {
171
+ return {
172
+ ok: false,
173
+ error: {
174
+ code: serverError.code || codeFromStatus(response.status),
175
+ message: serverError.message || "Request failed",
176
+ details: serverError.details && typeof serverError.details === "object" ? serverError.details : {}
177
+ },
178
+ meta: {
179
+ request_id: resolvedRequestId,
180
+ ...(typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) ? { retry_after_seconds: retryAfterSeconds } : {})
181
+ }
182
+ };
183
+ }
184
+
185
+ return buildStableError({
186
+ code: codeFromStatus(response.status),
187
+ message: response.statusText || "Request failed",
188
+ requestId: resolvedRequestId,
189
+ retryAfterSeconds
190
+ });
191
+ } catch (error) {
192
+ clearTimeout(timeout);
193
+ const isAbort = error && typeof error === "object" && error.name === "AbortError";
194
+ return buildStableError({
195
+ code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
196
+ message: isAbort ? "Request timed out" : "Network error",
197
+ requestId: resolvedRequestId
198
+ });
199
+ }
200
+ }
@@ -0,0 +1,136 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export function stripJsonc(input) {
5
+ const s = String(input || "");
6
+ let out = "";
7
+ let inString = false;
8
+ let quote = '"';
9
+ let escape = false;
10
+
11
+ for (let i = 0; i < s.length; i++) {
12
+ const ch = s[i];
13
+ const next = i + 1 < s.length ? s[i + 1] : "";
14
+
15
+ if (inString) {
16
+ out += ch;
17
+ if (escape) {
18
+ escape = false;
19
+ continue;
20
+ }
21
+ if (ch === "\\") {
22
+ escape = true;
23
+ continue;
24
+ }
25
+ if (ch === quote) {
26
+ inString = false;
27
+ }
28
+ continue;
29
+ }
30
+
31
+ if (ch === '"' || ch === "'") {
32
+ inString = true;
33
+ quote = ch;
34
+ out += ch;
35
+ continue;
36
+ }
37
+
38
+ // Line comment: //
39
+ if (ch === "/" && next === "/") {
40
+ i++;
41
+ while (i + 1 < s.length && s[i + 1] !== "\n") i++;
42
+ continue;
43
+ }
44
+
45
+ // Block comment: /* ... */
46
+ if (ch === "/" && next === "*") {
47
+ i += 2;
48
+ while (i < s.length) {
49
+ if (s[i] === "*" && i + 1 < s.length && s[i + 1] === "/") {
50
+ i++;
51
+ break;
52
+ }
53
+ i++;
54
+ }
55
+ continue;
56
+ }
57
+
58
+ out += ch;
59
+ }
60
+
61
+ return out;
62
+ }
63
+
64
+ function removeTrailingCommas(input) {
65
+ let s = String(input || "");
66
+ // Repeatedly remove ", }" and ", ]" occurrences until stable.
67
+ for (let i = 0; i < 10; i++) {
68
+ const next = s.replace(/,\s*([}\]])/g, "$1");
69
+ if (next === s) break;
70
+ s = next;
71
+ }
72
+ return s;
73
+ }
74
+
75
+ export function parseJsonLike(input) {
76
+ const raw = String(input || "");
77
+ try {
78
+ return JSON.parse(raw);
79
+ } catch {
80
+ // Best-effort JSONC parsing for configs with comments / trailing commas.
81
+ const cleaned = removeTrailingCommas(stripJsonc(raw));
82
+ return JSON.parse(cleaned);
83
+ }
84
+ }
85
+
86
+ export function formatJson(obj) {
87
+ return `${JSON.stringify(obj, null, 2)}\n`;
88
+ }
89
+
90
+ export function ensureObject(value) {
91
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
92
+ return {};
93
+ }
94
+
95
+ export function buildClawdealsServerConfig({ serverPath, apiKey, apiBase, origin, timeoutMs }) {
96
+ return {
97
+ type: "stdio",
98
+ command: "node",
99
+ args: [serverPath],
100
+ env: {
101
+ CLAWDEALS_API_KEY: apiKey,
102
+ CLAWDEALS_API_BASE: apiBase,
103
+ CLAWDEALS_ORIGIN: origin,
104
+ CLAWDEALS_TIMEOUT_MS: timeoutMs
105
+ }
106
+ };
107
+ }
108
+
109
+ export function buildClawdealsNpxServerConfig({ packageName = "clawdeals-mcp", apiKey, apiBase, origin, timeoutMs }) {
110
+ return {
111
+ type: "stdio",
112
+ command: "npx",
113
+ args: ["-y", packageName],
114
+ env: {
115
+ CLAWDEALS_API_KEY: apiKey,
116
+ CLAWDEALS_API_BASE: apiBase,
117
+ CLAWDEALS_ORIGIN: origin,
118
+ CLAWDEALS_TIMEOUT_MS: timeoutMs
119
+ }
120
+ };
121
+ }
122
+
123
+ export function upsertServer({ config, keyName, serverName, serverConfig }) {
124
+ const root = ensureObject(config);
125
+ const container = ensureObject(root[keyName]);
126
+ container[serverName] = serverConfig;
127
+ root[keyName] = container;
128
+ return root;
129
+ }
130
+
131
+ export function resolveRepoServerPath({ installScriptUrl }) {
132
+ // <pkg>/mcp/install.mjs -> <pkg>/mcp-server.mjs
133
+ const installFilePath = fileURLToPath(installScriptUrl);
134
+ const installDir = path.dirname(installFilePath);
135
+ return path.resolve(installDir, "..", "mcp-server.mjs");
136
+ }
@@ -0,0 +1,258 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ buildClawdealsServerConfig,
7
+ buildClawdealsNpxServerConfig,
8
+ ensureObject,
9
+ formatJson,
10
+ parseJsonLike,
11
+ upsertServer
12
+ } from "./install-lib.mjs";
13
+
14
+ function fail(message) {
15
+ console.error(`mcp:install: ${message}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ function existsFile(p) {
20
+ try {
21
+ const st = fs.statSync(p);
22
+ return st.isFile();
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function existsDir(p) {
29
+ try {
30
+ const st = fs.statSync(p);
31
+ return st.isDirectory();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function mkdirp(p) {
38
+ fs.mkdirSync(p, { recursive: true });
39
+ }
40
+
41
+ function nowStamp() {
42
+ const d = new Date();
43
+ const pad = (n) => String(n).padStart(2, "0");
44
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(
45
+ d.getSeconds()
46
+ )}`;
47
+ }
48
+
49
+ function readUtf8(p) {
50
+ return fs.readFileSync(p, "utf8");
51
+ }
52
+
53
+ function writeUtf8(p, content) {
54
+ fs.writeFileSync(p, content, "utf8");
55
+ }
56
+
57
+ function backupIfExists(filePath) {
58
+ if (!existsFile(filePath)) return null;
59
+ const backupPath = `${filePath}.bak-${nowStamp()}`;
60
+ fs.copyFileSync(filePath, backupPath);
61
+ return backupPath;
62
+ }
63
+
64
+ function detectCursorConfigPath() {
65
+ const home = os.homedir();
66
+ const cursorDir = path.join(home, ".cursor");
67
+ if (!existsDir(cursorDir)) return null;
68
+
69
+ const json = path.join(cursorDir, "mcp.json");
70
+ const jsonc = path.join(cursorDir, "mcp.jsonc");
71
+ if (existsFile(json)) return { filePath: json, defaultKey: "servers" };
72
+ if (existsFile(jsonc)) return { filePath: jsonc, defaultKey: "servers" };
73
+
74
+ // Cursor directory exists but config doesn't; create mcp.json.
75
+ return { filePath: json, defaultKey: "servers" };
76
+ }
77
+
78
+ function detectClaudeDesktopConfigPath() {
79
+ const platform = process.platform;
80
+ const home = os.homedir();
81
+
82
+ if (platform === "darwin") {
83
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
84
+ }
85
+ if (platform === "win32") {
86
+ const appData = process.env.APPDATA;
87
+ if (!appData) return null;
88
+ return path.join(appData, "Claude", "claude_desktop_config.json");
89
+ }
90
+
91
+ // Linux / others
92
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
93
+ return path.join(xdg, "Claude", "claude_desktop_config.json");
94
+ }
95
+
96
+ function chooseKeyName(parsed, defaultKey) {
97
+ const root = ensureObject(parsed);
98
+ if (root.mcpServers && typeof root.mcpServers === "object") return "mcpServers";
99
+ if (root.servers && typeof root.servers === "object") return "servers";
100
+ return defaultKey;
101
+ }
102
+
103
+ function loadOrInitConfig(filePath, defaultKey) {
104
+ if (!existsFile(filePath)) return { [defaultKey]: {} };
105
+
106
+ const raw = readUtf8(filePath);
107
+ // Treat an existing-but-empty file as "no config yet" so onboarding works.
108
+ // parseJsonLike("") throws, which would otherwise cause a misleading "skip".
109
+ const cleaned = String(raw || "").replace(/^\uFEFF/, "");
110
+ if (!cleaned.trim()) return { [defaultKey]: {} };
111
+
112
+ const parsed = parseJsonLike(cleaned);
113
+ if (!parsed || typeof parsed !== "object") return { [defaultKey]: {} };
114
+ return parsed;
115
+ }
116
+
117
+ function installIntoFile({ filePath, defaultKey, serverName, serverConfig, dryRun }) {
118
+ let parsed;
119
+ try {
120
+ parsed = loadOrInitConfig(filePath, defaultKey);
121
+ } catch (err) {
122
+ console.error(`mcp:install: Skipping (unparseable JSON/JSONC): ${filePath}`);
123
+ console.error(`mcp:install: ${String(err?.message || err)}`);
124
+ return { ok: false, skipped: true, filePath, reason: "unparseable" };
125
+ }
126
+
127
+ const keyName = chooseKeyName(parsed, defaultKey);
128
+ const next = upsertServer({ config: parsed, keyName, serverName, serverConfig });
129
+
130
+ if (dryRun) {
131
+ console.log(`mcp:install: (dry-run) would write ${filePath}`);
132
+ return { ok: true, filePath, keyName, backupPath: null, wrote: false };
133
+ }
134
+
135
+ const dir = path.dirname(filePath);
136
+ if (!existsDir(dir)) mkdirp(dir);
137
+
138
+ const backupPath = backupIfExists(filePath);
139
+ writeUtf8(filePath, formatJson(next));
140
+ return { ok: true, filePath, keyName, backupPath, wrote: true };
141
+ }
142
+
143
+ async function main() {
144
+ const argv = process.argv.slice(2);
145
+ const dryRun = argv.includes("--dry-run");
146
+ const local = argv.includes("--local") || argv.includes("--repo");
147
+ const fileArgIdx = argv.findIndex((a) => a === "--file" || a === "--path");
148
+ const explicitFile = fileArgIdx >= 0 ? argv[fileArgIdx + 1] : null;
149
+ if (fileArgIdx >= 0 && !explicitFile) {
150
+ fail("Missing value for --file (expected a path to a config file)");
151
+ }
152
+
153
+ const serverPathIdx = argv.findIndex((a) => a === "--server-path");
154
+ const explicitServerPath = serverPathIdx >= 0 ? argv[serverPathIdx + 1] : null;
155
+ if (serverPathIdx >= 0 && !explicitServerPath) {
156
+ fail("Missing value for --server-path (expected an absolute path to a local mcp-server.mjs)");
157
+ }
158
+
159
+ const packageIdx = argv.findIndex((a) => a === "--package");
160
+ const packageName = packageIdx >= 0 ? argv[packageIdx + 1] : "clawdeals-mcp";
161
+ if (packageIdx >= 0 && !packageName) {
162
+ fail("Missing value for --package (expected an npm package name)");
163
+ }
164
+
165
+ const apiKey = String(process.env.CLAWDEALS_API_KEY || "").trim();
166
+ if (!apiKey) fail("CLAWDEALS_API_KEY is required");
167
+
168
+ const apiBase = String(process.env.CLAWDEALS_API_BASE || "").trim();
169
+ if (!apiBase) fail("CLAWDEALS_API_BASE is required (example: https://app.clawdeals.com/api)");
170
+ const origin = String(process.env.CLAWDEALS_ORIGIN || "mcp").trim();
171
+ const timeoutMs = String(process.env.CLAWDEALS_TIMEOUT_MS || "15000").trim();
172
+
173
+ const serverConfig = local
174
+ ? (() => {
175
+ const raw = String(explicitServerPath || process.env.CLAWDEALS_MCP_SERVER_PATH || "").trim();
176
+ if (!raw) {
177
+ fail("Missing --server-path (or env CLAWDEALS_MCP_SERVER_PATH) for --local installs");
178
+ }
179
+ const serverPath = path.resolve(raw);
180
+ if (!existsFile(serverPath)) fail(`Missing server script: ${serverPath}`);
181
+ return buildClawdealsServerConfig({ serverPath, apiKey, apiBase, origin, timeoutMs });
182
+ })()
183
+ : buildClawdealsNpxServerConfig({ packageName, apiKey, apiBase, origin, timeoutMs });
184
+
185
+ const targets = [];
186
+
187
+ if (explicitFile) {
188
+ targets.push({ kind: "explicit", filePath: path.resolve(explicitFile), defaultKey: "servers" });
189
+ } else {
190
+ const cursor = detectCursorConfigPath();
191
+ if (cursor) targets.push({ kind: "cursor", ...cursor });
192
+
193
+ const claudePath = detectClaudeDesktopConfigPath();
194
+ if (claudePath && existsFile(claudePath)) {
195
+ targets.push({ kind: "claude-desktop", filePath: claudePath, defaultKey: "mcpServers" });
196
+ }
197
+ }
198
+
199
+ if (!targets.length) {
200
+ console.log("mcp:install: No supported MCP config file found.");
201
+ console.log("mcp:install: Supported: Cursor (~/.cursor/mcp.json) and Claude Desktop (claude_desktop_config.json).");
202
+ console.log("");
203
+ console.log("Manual config (Cursor-style):");
204
+ console.log(
205
+ formatJson({
206
+ servers: {
207
+ clawdeals: serverConfig
208
+ }
209
+ }).trimEnd()
210
+ );
211
+ process.exit(2);
212
+ }
213
+
214
+ const results = targets.map((t) =>
215
+ installIntoFile({
216
+ filePath: t.filePath,
217
+ defaultKey: t.defaultKey,
218
+ serverName: "clawdeals",
219
+ serverConfig,
220
+ dryRun
221
+ })
222
+ );
223
+
224
+ const ok = results.filter((r) => r.ok);
225
+ const wrote = results.filter((r) => r.ok && r.wrote);
226
+ const skipped = results.filter((r) => !r.ok && r.skipped);
227
+
228
+ for (const r of wrote) {
229
+ console.log(`mcp:install: Updated ${r.filePath} (${r.keyName}.clawdeals)`);
230
+ if (r.backupPath) console.log(`mcp:install: Backup ${r.backupPath}`);
231
+ }
232
+ for (const r of skipped) {
233
+ console.log(`mcp:install: Skipped ${r.filePath} (${r.reason})`);
234
+ }
235
+
236
+ if (!ok.length) {
237
+ console.error("mcp:install: No files were updated. All targets were skipped.");
238
+ process.exit(1);
239
+ }
240
+ if (!dryRun && wrote.length === 0) {
241
+ console.error("mcp:install: No files were updated.");
242
+ process.exit(1);
243
+ }
244
+
245
+ console.log("");
246
+ if (skipped.length) {
247
+ console.log(`mcp:install: Warning: ${skipped.length} target file(s) were skipped (see above).`);
248
+ console.log("");
249
+ }
250
+
251
+ if (dryRun ? ok.length : wrote.length) {
252
+ console.log(dryRun ? "mcp:install: Next steps (after running without --dry-run):" : "mcp:install: Next steps:");
253
+ console.log("mcp:install: 1) Restart your IDE so it reloads MCP servers.");
254
+ console.log('mcp:install: 2) In your IDE, call "clawdeals.deals.list" with { "limit": 1 }.');
255
+ }
256
+ }
257
+
258
+ main().catch((err) => fail(String(err?.stack || err?.message || err)));