@4da/mcp-server 6.0.1 → 6.0.2
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/CHANGELOG.md +51 -0
- package/README.md +6 -3
- package/dist/db.d.ts +16 -0
- package/dist/db.js +71 -23
- package/dist/index.js +11 -3
- package/dist/setup.d.ts +76 -3
- package/dist/setup.js +313 -130
- package/dist/tool-dispatch.d.ts +2 -0
- package/dist/tool-dispatch.js +21 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.0.2 — 2026-10-04
|
|
4
|
+
|
|
5
|
+
### `--setup` writes the files editors actually read
|
|
6
|
+
|
|
7
|
+
Three of the four editors `--setup` reported as configured were configured in
|
|
8
|
+
files those editors never read, so nothing happened:
|
|
9
|
+
|
|
10
|
+
| Editor | Before | Now |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| VS Code | `~/.vscode/mcp.json` | the user profile's `mcp.json`: `%APPDATA%\Code\User\` (Windows), `~/Library/Application Support/Code/User/` (macOS), `$XDG_CONFIG_HOME/Code/User/` (Linux, default `~/.config`), key `servers`, `"type": "stdio"` |
|
|
13
|
+
| Windsurf | `~/.windsurf/mcp.json` | `~/.codeium/windsurf/mcp_config.json`, key `mcpServers` |
|
|
14
|
+
| Claude Code | `mcpServers` in the project's `.claude/settings.json` (or, outside a project, rewrote `~/.claude.json`) | prints `claude mcp add --scope user 4da -- npx @4da/mcp-server`; `~/.claude.json` is never edited |
|
|
15
|
+
|
|
16
|
+
Added: VS Code Insiders, Claude Desktop (`claude_desktop_config.json`) and
|
|
17
|
+
Devin Desktop / Devin CLI (`~/.config/devin/mcp_config.json`,
|
|
18
|
+
`%APPDATA%\devin\` on Windows), Windsurf's successor. Cursor
|
|
19
|
+
(`~/.cursor/mcp.json`) was already right.
|
|
20
|
+
|
|
21
|
+
- **A config file setup cannot parse is left untouched.** It used to be read
|
|
22
|
+
as `{}`, so a VS Code `mcp.json` with a comment or a trailing comma (both
|
|
23
|
+
legal there) was replaced by our entry alone, deleting every other server.
|
|
24
|
+
Files are now parsed as JSONC and edited in place with `jsonc-parser`
|
|
25
|
+
(Microsoft's, the parser VS Code uses; no dependencies of its own), which
|
|
26
|
+
keeps comments, formatting, other servers and other keys. A file that does
|
|
27
|
+
not parse is reported with the entry to add by hand.
|
|
28
|
+
- An existing `4da` entry is updated, not duplicated, and keeps its other
|
|
29
|
+
fields (`env`); an identical one is left alone, so a second run writes
|
|
30
|
+
nothing.
|
|
31
|
+
- Every modified file is backed up first (`<file>.bak`; an earlier backup is
|
|
32
|
+
never overwritten) and replaced atomically (temp file, then rename).
|
|
33
|
+
- `--setup --dry-run` wrote anyway when run through `npx @4da/mcp-server`
|
|
34
|
+
(only the `4da-mcp-setup` entry honoured the flag). It now writes nothing on
|
|
35
|
+
either, and prints each file and the exact entry it would add.
|
|
36
|
+
|
|
37
|
+
### Clearer answers when the database cannot help
|
|
38
|
+
|
|
39
|
+
- A desktop-only tool (`get_relevant_content`, `get_actionable_signals`,
|
|
40
|
+
`knowledge_gaps`, `record_feedback`, `developer_dna`) called without the
|
|
41
|
+
desktop app's database answered `[]`, which reads as "nothing relevant". It
|
|
42
|
+
now carries a `desktop_app_note` saying it needs the app.
|
|
43
|
+
- A database file that is damaged or not SQLite answered every call with
|
|
44
|
+
SQLite's bare "file is not a database". The message now names the file and
|
|
45
|
+
what to do, and never advises deleting it (`--doctor`'s integrity check used
|
|
46
|
+
to say "Try deleting data/4da.db"). The handle a failed open left behind is
|
|
47
|
+
closed, so on Windows the file can be moved aside while the server runs.
|
|
48
|
+
- The database lookup no longer checks `data/4da.db` two directories above the
|
|
49
|
+
server's own files, a leftover from when the server was a folder of the app
|
|
50
|
+
repository; installed, it pointed into `node_modules/@4da/`.
|
|
51
|
+
|
|
52
|
+
- Dependencies: `@modelcontextprotocol/server` ^2.2.0, `hono` ^4.13.12; dev: `@types/node` ^26.6.3, `vitest` ^5.0.3 (#2).
|
|
53
|
+
|
|
3
54
|
## 6.0.1 — 2026-10-04
|
|
4
55
|
|
|
5
56
|
### The server has its own repository
|
package/README.md
CHANGED
|
@@ -45,9 +45,9 @@ claude plugin install 4da@4da
|
|
|
45
45
|
The hook: when your agent edits a dependency's version in `package.json`, `Cargo.toml`, `pyproject.toml`, `requirements.txt` or `go.mod`, it is told which packages moved and given the exact `dependency_check` and `upgrade_impact` calls to make before it installs and builds. The hook is plain Node, contacts nothing, and stays silent for every other edit.
|
|
46
46
|
|
|
47
47
|
<details>
|
|
48
|
-
<summary><b>Cursor / Windsurf</b></summary>
|
|
48
|
+
<summary><b>Cursor / Windsurf / Devin Desktop</b></summary>
|
|
49
49
|
|
|
50
|
-
Add to `~/.cursor/mcp.json`
|
|
50
|
+
Add to `~/.cursor/mcp.json` (Cursor), `~/.codeium/windsurf/mcp_config.json` (Windsurf), or `~/.config/devin/mcp_config.json` (Devin Desktop and Devin CLI; `%APPDATA%devinmcp_config.json` on Windows):
|
|
51
51
|
```json
|
|
52
52
|
{
|
|
53
53
|
"mcpServers": {
|
|
@@ -63,7 +63,7 @@ Add to `~/.cursor/mcp.json` or `~/.windsurf/mcp.json`:
|
|
|
63
63
|
<details>
|
|
64
64
|
<summary><b>VS Code (Copilot)</b></summary>
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Run **MCP: Open User Configuration** and add the entry, or edit the file directly: `%APPDATA%CodeUsermcp.json` (Windows), `~/Library/Application Support/Code/User/mcp.json` (macOS), `~/.config/Code/User/mcp.json` (Linux). VS Code uses `servers`, not `mcpServers`:
|
|
67
67
|
```json
|
|
68
68
|
{
|
|
69
69
|
"servers": {
|
|
@@ -97,8 +97,11 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
97
97
|
<summary><b>Auto-setup</b> (detects all installed editors)</summary>
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
+
npx @4da/mcp-server --setup --dry-run # show each file and entry, write nothing
|
|
100
101
|
npx @4da/mcp-server --setup
|
|
101
102
|
```
|
|
103
|
+
|
|
104
|
+
Writes Cursor, VS Code (and Insiders), Windsurf, Devin Desktop and Claude Desktop configs it finds, merging into what is there: other servers, settings and comments are kept, a backup (`.bak`) is written first, and a file it cannot parse is left untouched with the entry to add by hand. For Claude Code it prints the `claude mcp add` command instead of editing `~/.claude.json`.
|
|
102
105
|
</details>
|
|
103
106
|
|
|
104
107
|
Then ask your AI: **"What changes if I upgrade X to Y?"**, **"Scan for vulnerabilities"** or **"What should I upgrade first?"**
|
package/dist/db.d.ts
CHANGED
|
@@ -30,6 +30,20 @@ export declare function desktopAppDbPaths(platform?: NodeJS.Platform, env?: Node
|
|
|
30
30
|
* in, so agent memory and decisions did not follow the user between projects.
|
|
31
31
|
*/
|
|
32
32
|
export declare function standaloneDbPath(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether an error says the database file itself is unreadable: not SQLite at
|
|
35
|
+
* all, or damaged. better-sqlite3 reports these as SQLITE_NOTADB ("file is not
|
|
36
|
+
* a database") and SQLITE_CORRUPT ("database disk image is malformed").
|
|
37
|
+
*/
|
|
38
|
+
export declare function isUnreadableDbError(error: unknown): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* What to tell the user about an unreadable database. Every tool call used to
|
|
41
|
+
* answer with the bare SQLite text ("file is not a database"), with no way
|
|
42
|
+
* out. Nothing is deleted or moved automatically: the desktop app's database
|
|
43
|
+
* can be the user's whole corpus, and the standalone one holds their recorded
|
|
44
|
+
* decisions and agent memory.
|
|
45
|
+
*/
|
|
46
|
+
export declare function unreadableDbMessage(absolutePath: string, error: unknown): string;
|
|
33
47
|
export interface DatabaseValidationResult {
|
|
34
48
|
valid: boolean;
|
|
35
49
|
error?: string;
|
|
@@ -98,6 +112,8 @@ export declare const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, lan
|
|
|
98
112
|
export declare class FourDADatabase {
|
|
99
113
|
private db;
|
|
100
114
|
private _isStandalone;
|
|
115
|
+
/** Absolute path of the open database file. */
|
|
116
|
+
readonly dbPath: string;
|
|
101
117
|
constructor(dbPath?: string);
|
|
102
118
|
/**
|
|
103
119
|
* Whether this database was freshly created in standalone mode
|
package/dist/db.js
CHANGED
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
import path from "path";
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as os from "os";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
11
|
import { checkNativeBindings, isNativeBindingError, nativeBindingMessage } from "./native-bindings.js";
|
|
14
12
|
let Database;
|
|
15
13
|
try {
|
|
@@ -23,38 +21,37 @@ catch (err) {
|
|
|
23
21
|
export function nativeBindingProblem() {
|
|
24
22
|
return checkNativeBindings(Database);
|
|
25
23
|
}
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
-
const __dirname = dirname(__filename);
|
|
28
24
|
import { dbRecoveryNote, readDbRecoveredMarker, readEngineBlockMarker } from "./freshness-markers.js";
|
|
29
25
|
/**
|
|
30
26
|
* Resolve the database path by checking multiple locations in priority order:
|
|
31
27
|
* 1. FOURDA_DB_PATH env var
|
|
32
|
-
* 2. data/4da.db relative to cwd
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
* 2. data/4da.db relative to cwd: the desktop app's development database,
|
|
29
|
+
* when the server is started in a checkout of the app's repository
|
|
30
|
+
* 3. The deployed desktop app's database (desktopAppDbPaths)
|
|
31
|
+
* 4. Otherwise the standalone database (standaloneDbPath), created on first use
|
|
32
|
+
*
|
|
33
|
+
* Until 6.0.2 a step between 2 and 3 looked for data/4da.db two directories
|
|
34
|
+
* above this file, which found the app's database while this server was the
|
|
35
|
+
* app repository's mcp-4da-server/ folder. Since the server moved to its own
|
|
36
|
+
* repository (6.0.1) that resolves to node_modules/@4da/ or to the folder
|
|
37
|
+
* holding a clone, never to an app database, so it was removed.
|
|
36
38
|
*/
|
|
37
39
|
function getDefaultDbPath() {
|
|
38
40
|
// 1. Environment variable (highest priority)
|
|
39
41
|
if (process.env.FOURDA_DB_PATH) {
|
|
40
42
|
return process.env.FOURDA_DB_PATH;
|
|
41
43
|
}
|
|
42
|
-
// 2. Relative to cwd (development)
|
|
44
|
+
// 2. Relative to cwd (the app repository's development database)
|
|
43
45
|
const cwdPath = path.resolve(process.cwd(), "data", "4da.db");
|
|
44
46
|
if (fs.existsSync(cwdPath)) {
|
|
45
47
|
return cwdPath;
|
|
46
48
|
}
|
|
47
|
-
// 3.
|
|
48
|
-
const projectRootPath = path.resolve(__dirname, "..", "..", "data", "4da.db");
|
|
49
|
-
if (fs.existsSync(projectRootPath)) {
|
|
50
|
-
return projectRootPath;
|
|
51
|
-
}
|
|
52
|
-
// 4. The desktop app's own database, where the deployed app keeps it.
|
|
49
|
+
// 3. The desktop app's own database, where the deployed app keeps it.
|
|
53
50
|
for (const appDataPath of desktopAppDbPaths()) {
|
|
54
51
|
if (fs.existsSync(appDataPath))
|
|
55
52
|
return appDataPath;
|
|
56
53
|
}
|
|
57
|
-
//
|
|
54
|
+
// 4. No desktop database: the standalone database, in the user's data dir.
|
|
58
55
|
return standaloneDbPath();
|
|
59
56
|
}
|
|
60
57
|
/**
|
|
@@ -104,6 +101,38 @@ export function standaloneDbPath() {
|
|
|
104
101
|
: process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
105
102
|
return path.join(base, "4da-mcp", "standalone.db");
|
|
106
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether an error says the database file itself is unreadable: not SQLite at
|
|
106
|
+
* all, or damaged. better-sqlite3 reports these as SQLITE_NOTADB ("file is not
|
|
107
|
+
* a database") and SQLITE_CORRUPT ("database disk image is malformed").
|
|
108
|
+
*/
|
|
109
|
+
export function isUnreadableDbError(error) {
|
|
110
|
+
const code = error?.code;
|
|
111
|
+
if (code === "SQLITE_NOTADB" || code === "SQLITE_CORRUPT")
|
|
112
|
+
return true;
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
return /file is not a database|database disk image is malformed/i.test(message);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What to tell the user about an unreadable database. Every tool call used to
|
|
118
|
+
* answer with the bare SQLite text ("file is not a database"), with no way
|
|
119
|
+
* out. Nothing is deleted or moved automatically: the desktop app's database
|
|
120
|
+
* can be the user's whole corpus, and the standalone one holds their recorded
|
|
121
|
+
* decisions and agent memory.
|
|
122
|
+
*/
|
|
123
|
+
export function unreadableDbMessage(absolutePath, error) {
|
|
124
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
125
|
+
const isStandaloneFile = path.resolve(absolutePath) === path.resolve(standaloneDbPath());
|
|
126
|
+
const remedy = isStandaloneFile
|
|
127
|
+
? "It is this server's standalone database: move it aside (keep it if it holds decisions or agent " +
|
|
128
|
+
"memory you want) and the next call creates a new one."
|
|
129
|
+
: process.env.FOURDA_DB_PATH
|
|
130
|
+
? "FOURDA_DB_PATH points at it: point it at a valid 4DA database, or unset it to use the default."
|
|
131
|
+
: "If it is the 4DA desktop app's database, start the app, which checks its database on startup. " +
|
|
132
|
+
"Do not delete it: it may be your only copy of the app's data.";
|
|
133
|
+
return (`The 4DA database at ${absolutePath} cannot be read (${detail}): the file is damaged or is not ` +
|
|
134
|
+
`a SQLite database. ${remedy} Run \`npx @4da/mcp-server --doctor\` for details.`);
|
|
135
|
+
}
|
|
107
136
|
/** Marker table written into every standalone database. */
|
|
108
137
|
const STANDALONE_MARKER = "mcp_standalone";
|
|
109
138
|
/**
|
|
@@ -200,6 +229,8 @@ export const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, language, p
|
|
|
200
229
|
export class FourDADatabase {
|
|
201
230
|
db;
|
|
202
231
|
_isStandalone = false;
|
|
232
|
+
/** Absolute path of the open database file. */
|
|
233
|
+
dbPath;
|
|
203
234
|
constructor(dbPath) {
|
|
204
235
|
const resolvedPath = dbPath || getDefaultDbPath();
|
|
205
236
|
// Resolve path - if relative, resolve from cwd
|
|
@@ -214,15 +245,29 @@ export class FourDADatabase {
|
|
|
214
245
|
fs.mkdirSync(dir, { recursive: true });
|
|
215
246
|
}
|
|
216
247
|
}
|
|
248
|
+
let opened = null;
|
|
217
249
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
250
|
+
opened = new Database(absolutePath, { readonly: false }); // Need write for feedback
|
|
251
|
+
opened.pragma("journal_mode = WAL");
|
|
220
252
|
}
|
|
221
253
|
catch (error) {
|
|
254
|
+
// Close the handle a failed pragma leaves open: every tool call retries
|
|
255
|
+
// the open, and on Windows each leaked handle also keeps the file locked
|
|
256
|
+
// against the move-aside the message below suggests.
|
|
257
|
+
try {
|
|
258
|
+
opened?.close();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Nothing to close when the open itself failed.
|
|
262
|
+
}
|
|
222
263
|
throw new Error(isNativeBindingError(error)
|
|
223
264
|
? nativeBindingMessage(error)
|
|
224
|
-
:
|
|
265
|
+
: isUnreadableDbError(error)
|
|
266
|
+
? unreadableDbMessage(absolutePath, error)
|
|
267
|
+
: `Failed to open 4DA database at ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
268
|
}
|
|
269
|
+
this.db = opened;
|
|
270
|
+
this.dbPath = absolutePath;
|
|
226
271
|
// Standalone mode: create schema for a brand-new database, and recognise
|
|
227
272
|
// an existing standalone database as one on every later session.
|
|
228
273
|
if (isNew) {
|
|
@@ -302,8 +347,10 @@ export class FourDADatabase {
|
|
|
302
347
|
if (integrityStatus !== "ok") {
|
|
303
348
|
return {
|
|
304
349
|
valid: false,
|
|
305
|
-
|
|
306
|
-
|
|
350
|
+
// It used to say "Try deleting data/4da.db": a path that is not
|
|
351
|
+
// where the app keeps its database, and advice that destroys the
|
|
352
|
+
// user's only copy of it.
|
|
353
|
+
error: unreadableDbMessage(absolutePath, new Error(`integrity check failed: ${integrityStatus}`)),
|
|
307
354
|
};
|
|
308
355
|
}
|
|
309
356
|
}
|
|
@@ -322,8 +369,9 @@ export class FourDADatabase {
|
|
|
322
369
|
catch (error) {
|
|
323
370
|
return {
|
|
324
371
|
valid: false,
|
|
325
|
-
error:
|
|
326
|
-
|
|
372
|
+
error: isUnreadableDbError(error)
|
|
373
|
+
? unreadableDbMessage(absolutePath, error)
|
|
374
|
+
: `Failed to open database at ${absolutePath}: ` + (error instanceof Error ? error.message : String(error)),
|
|
327
375
|
};
|
|
328
376
|
}
|
|
329
377
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -68,7 +68,7 @@ const SERVER_VERSION = (() => {
|
|
|
68
68
|
return "0.0.0";
|
|
69
69
|
}
|
|
70
70
|
})();
|
|
71
|
-
import { createDatabase, DEPENDENCY_GROUP_QUERY, FourDADatabase } from "./db.js";
|
|
71
|
+
import { createDatabase, DEPENDENCY_GROUP_QUERY, FourDADatabase, isUnreadableDbError, unreadableDbMessage, } from "./db.js";
|
|
72
72
|
// =============================================================================
|
|
73
73
|
// Server Setup
|
|
74
74
|
// =============================================================================
|
|
@@ -376,7 +376,14 @@ export function buildServer() {
|
|
|
376
376
|
return await dispatchTool(name, database, args);
|
|
377
377
|
}
|
|
378
378
|
catch (error) {
|
|
379
|
-
|
|
379
|
+
// A database that turns out damaged mid-session (SQLITE_CORRUPT on a
|
|
380
|
+
// page the open did not touch) gets the same guidance as one that
|
|
381
|
+
// fails to open, instead of the bare SQLite text.
|
|
382
|
+
const errorMessage = db && isUnreadableDbError(error)
|
|
383
|
+
? unreadableDbMessage(db.dbPath, error)
|
|
384
|
+
: error instanceof Error
|
|
385
|
+
? error.message
|
|
386
|
+
: String(error);
|
|
380
387
|
return {
|
|
381
388
|
content: [
|
|
382
389
|
{
|
|
@@ -441,6 +448,7 @@ async function main() {
|
|
|
441
448
|
refused unless MCP_AUTH_SECRET is set; authentication
|
|
442
449
|
is then mandatory for every request.
|
|
443
450
|
--setup Detect editors and write MCP config
|
|
451
|
+
--dry-run With --setup: print each file and entry, write nothing
|
|
444
452
|
--doctor Validate database, bindings, and LLM providers
|
|
445
453
|
|
|
446
454
|
Environment:
|
|
@@ -463,7 +471,7 @@ async function main() {
|
|
|
463
471
|
}
|
|
464
472
|
// Setup command: configure editors
|
|
465
473
|
if (args.includes("--setup") || args.includes("setup")) {
|
|
466
|
-
runSetup();
|
|
474
|
+
runSetup(args.includes("--dry-run"));
|
|
467
475
|
return;
|
|
468
476
|
}
|
|
469
477
|
// Doctor command: validate installation health
|
package/dist/setup.d.ts
CHANGED
|
@@ -6,12 +6,85 @@
|
|
|
6
6
|
* the user can start using 4DA tools immediately after install.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx @4da/mcp-server --setup
|
|
10
|
-
* 4da-mcp-setup
|
|
9
|
+
* npx @4da/mcp-server --setup [--dry-run]
|
|
10
|
+
* 4da-mcp-setup [--dry-run]
|
|
11
11
|
* pnpm run setup
|
|
12
|
+
*
|
|
13
|
+
* Audited 2026-10-05: three of the four editors were configured in files the
|
|
14
|
+
* editor never reads (`~/.vscode/mcp.json`, `~/.windsurf/mcp.json`, and
|
|
15
|
+
* `mcpServers` in a project's `.claude/settings.json`), and any config file
|
|
16
|
+
* that failed `JSON.parse` (VS Code's `mcp.json` is JSONC: comments and
|
|
17
|
+
* trailing commas are legal) was replaced by `{}` plus our entry, deleting
|
|
18
|
+
* every other server the user had. Each path below cites the doc it was
|
|
19
|
+
* checked against, and a file that cannot be parsed is never written.
|
|
20
|
+
*/
|
|
21
|
+
/** The Claude Code command printed instead of editing `~/.claude.json`. */
|
|
22
|
+
export declare const CLAUDE_CODE_COMMAND = "claude mcp add --scope user 4da -- npx @4da/mcp-server";
|
|
23
|
+
/** Where the paths are resolved: injected so tests can stand in any platform. */
|
|
24
|
+
export interface SetupContext {
|
|
25
|
+
platform: NodeJS.Platform;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
home: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function currentContext(): SetupContext;
|
|
30
|
+
/** An editor whose MCP config file this setup writes. */
|
|
31
|
+
export interface EditorTarget {
|
|
32
|
+
name: string;
|
|
33
|
+
/** Config file the server entry goes into. */
|
|
34
|
+
configPath: string;
|
|
35
|
+
/** Directory whose existence means the editor is installed. */
|
|
36
|
+
detectDir: string;
|
|
37
|
+
/** Top-level key holding the servers: VS Code uses `servers`, the rest `mcpServers`. */
|
|
38
|
+
key: "mcpServers" | "servers";
|
|
39
|
+
/** The value written under `<key>["4da"]`. */
|
|
40
|
+
entry: Record<string, unknown>;
|
|
41
|
+
/** Where the path and key were verified. */
|
|
42
|
+
docs: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Every editor this setup can write, with its config path on `ctx.platform`.
|
|
46
|
+
* Pure: nothing is read from disk here.
|
|
47
|
+
*/
|
|
48
|
+
export declare function editorTargets(ctx?: SetupContext): EditorTarget[];
|
|
49
|
+
/**
|
|
50
|
+
* Claude Code keeps user- and local-scope servers in `~/.claude.json`
|
|
51
|
+
* (`$CLAUDE_CONFIG_DIR/.claude.json` when set), a file it rewrites itself and
|
|
52
|
+
* that also holds the sign-in session and per-project state. Setup never
|
|
53
|
+
* edits it: it prints the `claude mcp add` command, which does.
|
|
54
|
+
* https://code.claude.com/docs/en/mcp
|
|
55
|
+
*/
|
|
56
|
+
export declare function claudeCodeConfigPath(ctx?: SetupContext): string;
|
|
57
|
+
export type PlanAction = "create" | "update" | "unchanged" | "refuse";
|
|
58
|
+
export interface ConfigPlan {
|
|
59
|
+
action: PlanAction;
|
|
60
|
+
/** The full new file content for create/update. */
|
|
61
|
+
content?: string;
|
|
62
|
+
/** Why a file is refused, for the user. */
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Decide what writing `entry` under `<key>["4da"]` does to a config file whose
|
|
67
|
+
* current text is `existing` (null when it does not exist).
|
|
68
|
+
*
|
|
69
|
+
* The file is parsed as JSONC (comments and trailing commas allowed) and
|
|
70
|
+
* edited in place with `jsonc-parser`'s `modify`, so other servers, other keys,
|
|
71
|
+
* comments and formatting all survive. A file that does not parse is refused,
|
|
72
|
+
* never replaced. An existing `4da` entry keeps its other fields (`env`, say);
|
|
73
|
+
* only the fields setup owns are set.
|
|
74
|
+
*/
|
|
75
|
+
export declare function planConfig(existing: string | null, key: string, entry: Record<string, unknown>): ConfigPlan;
|
|
76
|
+
/**
|
|
77
|
+
* Write `content` to `filePath`: back up an existing file first, then write a
|
|
78
|
+
* temp file beside it and rename it over the original, so an interrupted run
|
|
79
|
+
* leaves either the old file or the new one, never half of one. A symlinked
|
|
80
|
+
* config (a dotfiles repo) is written through to its target.
|
|
81
|
+
* @returns The backup path, when one was made.
|
|
12
82
|
*/
|
|
83
|
+
export declare function writeConfigAtomic(filePath: string, content: string): string | null;
|
|
84
|
+
type Log = (line: string) => void;
|
|
13
85
|
/**
|
|
14
86
|
* Run the setup wizard: detect editors and write MCP configurations.
|
|
15
87
|
* @param dryRun If true, only report what would be written without modifying files.
|
|
16
88
|
*/
|
|
17
|
-
export declare function runSetup(dryRun?: boolean): void;
|
|
89
|
+
export declare function runSetup(dryRun?: boolean, ctx?: SetupContext, log?: Log): void;
|
|
90
|
+
export {};
|
package/dist/setup.js
CHANGED
|
@@ -7,160 +7,343 @@
|
|
|
7
7
|
* the user can start using 4DA tools immediately after install.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx @4da/mcp-server --setup
|
|
11
|
-
* 4da-mcp-setup
|
|
10
|
+
* npx @4da/mcp-server --setup [--dry-run]
|
|
11
|
+
* 4da-mcp-setup [--dry-run]
|
|
12
12
|
* pnpm run setup
|
|
13
|
+
*
|
|
14
|
+
* Audited 2026-10-05: three of the four editors were configured in files the
|
|
15
|
+
* editor never reads (`~/.vscode/mcp.json`, `~/.windsurf/mcp.json`, and
|
|
16
|
+
* `mcpServers` in a project's `.claude/settings.json`), and any config file
|
|
17
|
+
* that failed `JSON.parse` (VS Code's `mcp.json` is JSONC: comments and
|
|
18
|
+
* trailing commas are legal) was replaced by `{}` plus our entry, deleting
|
|
19
|
+
* every other server the user had. Each path below cites the doc it was
|
|
20
|
+
* checked against, and a file that cannot be parsed is never written.
|
|
13
21
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
-
import
|
|
22
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
16
24
|
import { homedir } from "node:os";
|
|
25
|
+
import jsonc from "jsonc-parser";
|
|
26
|
+
/** The command every editor runs. */
|
|
27
|
+
const SERVER_COMMAND = { command: "npx", args: ["@4da/mcp-server"] };
|
|
17
28
|
/** MCP config snippet for editors that use the standard mcpServers format */
|
|
18
29
|
const MCP_CONFIG = {
|
|
19
|
-
"4da":
|
|
20
|
-
command: "npx",
|
|
21
|
-
args: ["@4da/mcp-server"],
|
|
22
|
-
},
|
|
30
|
+
"4da": SERVER_COMMAND,
|
|
23
31
|
};
|
|
32
|
+
/** The Claude Code command printed instead of editing `~/.claude.json`. */
|
|
33
|
+
export const CLAUDE_CODE_COMMAND = "claude mcp add --scope user 4da -- npx @4da/mcp-server";
|
|
34
|
+
export function currentContext() {
|
|
35
|
+
return { platform: process.platform, env: process.env, home: homedir() };
|
|
36
|
+
}
|
|
37
|
+
function pathFor(ctx) {
|
|
38
|
+
return ctx.platform === "win32" ? path.win32 : path.posix;
|
|
39
|
+
}
|
|
40
|
+
/** `%APPDATA%` on Windows, the Electron-style config base elsewhere. */
|
|
41
|
+
function roamingDir(ctx) {
|
|
42
|
+
const p = pathFor(ctx);
|
|
43
|
+
return ctx.env.APPDATA?.trim() || p.join(ctx.home, "AppData", "Roaming");
|
|
44
|
+
}
|
|
45
|
+
function xdgConfigDir(ctx) {
|
|
46
|
+
return ctx.env.XDG_CONFIG_HOME?.trim() || pathFor(ctx).join(ctx.home, ".config");
|
|
47
|
+
}
|
|
24
48
|
/**
|
|
25
|
-
*
|
|
49
|
+
* The per-platform user-data dir of a desktop app (Electron `appData`):
|
|
50
|
+
* `%APPDATA%\<name>`, `~/Library/Application Support/<name>`,
|
|
51
|
+
* `$XDG_CONFIG_HOME/<name>` (default `~/.config/<name>`).
|
|
26
52
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
53
|
+
function appDataDir(ctx, name) {
|
|
54
|
+
const p = pathFor(ctx);
|
|
55
|
+
if (ctx.platform === "win32")
|
|
56
|
+
return p.join(roamingDir(ctx), name);
|
|
57
|
+
if (ctx.platform === "darwin")
|
|
58
|
+
return p.join(ctx.home, "Library", "Application Support", name);
|
|
59
|
+
return p.join(xdgConfigDir(ctx), name);
|
|
36
60
|
}
|
|
37
61
|
/**
|
|
38
|
-
*
|
|
62
|
+
* Every editor this setup can write, with its config path on `ctx.platform`.
|
|
63
|
+
* Pure: nothing is read from disk here.
|
|
39
64
|
*/
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "Cursor",
|
|
70
|
-
detect: () => {
|
|
71
|
-
const cursorDir = join(homedir(), ".cursor");
|
|
72
|
-
if (existsSync(cursorDir)) {
|
|
73
|
-
return join(cursorDir, "mcp.json");
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
},
|
|
77
|
-
write: (configPath) => {
|
|
78
|
-
const config = readJsonSafe(configPath);
|
|
79
|
-
const mcpServers = (config.mcpServers || {});
|
|
80
|
-
mcpServers["4da"] = MCP_CONFIG["4da"];
|
|
81
|
-
config.mcpServers = mcpServers;
|
|
82
|
-
writeJsonFile(configPath, config);
|
|
83
|
-
console.log(` Written: ${configPath}`);
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "VS Code (Copilot)",
|
|
88
|
-
detect: () => {
|
|
89
|
-
const vscodePath = join(homedir(), ".vscode");
|
|
90
|
-
if (existsSync(vscodePath)) {
|
|
91
|
-
return join(vscodePath, "mcp.json");
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
65
|
+
export function editorTargets(ctx = currentContext()) {
|
|
66
|
+
const p = pathFor(ctx);
|
|
67
|
+
const vscodeEntry = { type: "stdio", ...SERVER_COMMAND };
|
|
68
|
+
const vscode = (name, product) => {
|
|
69
|
+
const userDir = p.join(appDataDir(ctx, product), "User");
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
// The default profile's user-level file; it sits beside settings.json.
|
|
73
|
+
// A non-default profile keeps its own under User/profiles/<id>/, which
|
|
74
|
+
// VS Code does not name anywhere setup could find it.
|
|
75
|
+
configPath: p.join(userDir, "mcp.json"),
|
|
76
|
+
detectDir: userDir,
|
|
77
|
+
key: "servers",
|
|
78
|
+
entry: vscodeEntry,
|
|
79
|
+
docs: "https://code.visualstudio.com/docs/copilot/reference/mcp-configuration",
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const devinDir = ctx.platform === "win32" ? p.join(roamingDir(ctx), "devin") : p.join(xdgConfigDir(ctx), "devin");
|
|
83
|
+
const windsurfDir = p.join(ctx.home, ".codeium", "windsurf");
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
name: "Cursor",
|
|
87
|
+
configPath: p.join(ctx.home, ".cursor", "mcp.json"),
|
|
88
|
+
detectDir: p.join(ctx.home, ".cursor"),
|
|
89
|
+
key: "mcpServers",
|
|
90
|
+
entry: SERVER_COMMAND,
|
|
91
|
+
docs: "https://cursor.com/docs/mcp",
|
|
94
92
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
config.servers = servers;
|
|
105
|
-
writeJsonFile(configPath, config);
|
|
106
|
-
console.log(` Written: ${configPath}`);
|
|
93
|
+
vscode("VS Code (Copilot)", "Code"),
|
|
94
|
+
vscode("VS Code Insiders (Copilot)", "Code - Insiders"),
|
|
95
|
+
{
|
|
96
|
+
name: "Windsurf",
|
|
97
|
+
configPath: p.join(windsurfDir, "mcp_config.json"),
|
|
98
|
+
detectDir: windsurfDir,
|
|
99
|
+
key: "mcpServers",
|
|
100
|
+
entry: SERVER_COMMAND,
|
|
101
|
+
docs: "https://docs.devin.ai/cli/reference/configuration/read-config-from",
|
|
107
102
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
{
|
|
104
|
+
// Windsurf's successor. Its Cascade agent and the Devin CLI read the
|
|
105
|
+
// same user-level file.
|
|
106
|
+
name: "Devin Desktop / Devin CLI",
|
|
107
|
+
configPath: p.join(devinDir, "mcp_config.json"),
|
|
108
|
+
detectDir: devinDir,
|
|
109
|
+
key: "mcpServers",
|
|
110
|
+
entry: SERVER_COMMAND,
|
|
111
|
+
docs: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
117
112
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
{
|
|
114
|
+
name: "Claude Desktop",
|
|
115
|
+
configPath: p.join(appDataDir(ctx, "Claude"), "claude_desktop_config.json"),
|
|
116
|
+
detectDir: appDataDir(ctx, "Claude"),
|
|
117
|
+
key: "mcpServers",
|
|
118
|
+
entry: SERVER_COMMAND,
|
|
119
|
+
docs: "https://modelcontextprotocol.io/docs/develop/connect-local-servers",
|
|
125
120
|
},
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Claude Code keeps user- and local-scope servers in `~/.claude.json`
|
|
125
|
+
* (`$CLAUDE_CONFIG_DIR/.claude.json` when set), a file it rewrites itself and
|
|
126
|
+
* that also holds the sign-in session and per-project state. Setup never
|
|
127
|
+
* edits it: it prints the `claude mcp add` command, which does.
|
|
128
|
+
* https://code.claude.com/docs/en/mcp
|
|
129
|
+
*/
|
|
130
|
+
export function claudeCodeConfigPath(ctx = currentContext()) {
|
|
131
|
+
const p = pathFor(ctx);
|
|
132
|
+
const configDir = ctx.env.CLAUDE_CONFIG_DIR?.trim();
|
|
133
|
+
return configDir ? p.join(configDir, ".claude.json") : p.join(ctx.home, ".claude.json");
|
|
134
|
+
}
|
|
135
|
+
function lineOf(text, offset) {
|
|
136
|
+
return text.slice(0, offset).split("\n").length;
|
|
137
|
+
}
|
|
138
|
+
function isPlainObject(value) {
|
|
139
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
function sameJson(a, b) {
|
|
142
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Decide what writing `entry` under `<key>["4da"]` does to a config file whose
|
|
146
|
+
* current text is `existing` (null when it does not exist).
|
|
147
|
+
*
|
|
148
|
+
* The file is parsed as JSONC (comments and trailing commas allowed) and
|
|
149
|
+
* edited in place with `jsonc-parser`'s `modify`, so other servers, other keys,
|
|
150
|
+
* comments and formatting all survive. A file that does not parse is refused,
|
|
151
|
+
* never replaced. An existing `4da` entry keeps its other fields (`env`, say);
|
|
152
|
+
* only the fields setup owns are set.
|
|
153
|
+
*/
|
|
154
|
+
export function planConfig(existing, key, entry) {
|
|
155
|
+
if (existing === null || existing.trim() === "") {
|
|
156
|
+
return {
|
|
157
|
+
action: existing === null ? "create" : "update",
|
|
158
|
+
content: JSON.stringify({ [key]: { "4da": entry } }, null, 2) + "\n",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const errors = [];
|
|
162
|
+
const root = jsonc.parse(existing, errors, { allowTrailingComma: true, disallowComments: false });
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
const first = errors[0];
|
|
165
|
+
return {
|
|
166
|
+
action: "refuse",
|
|
167
|
+
reason: `it does not parse (${jsonc.printParseErrorCode(first.error)} at line ${lineOf(existing, first.offset)})`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!isPlainObject(root)) {
|
|
171
|
+
return { action: "refuse", reason: "its top level is not a JSON object" };
|
|
172
|
+
}
|
|
173
|
+
const servers = root[key];
|
|
174
|
+
if (servers !== undefined && !isPlainObject(servers)) {
|
|
175
|
+
return { action: "refuse", reason: `its "${key}" is not an object` };
|
|
176
|
+
}
|
|
177
|
+
const current = servers?.["4da"];
|
|
178
|
+
if (current !== undefined && !isPlainObject(current)) {
|
|
179
|
+
return { action: "refuse", reason: `its "${key}"."4da" is not an object` };
|
|
180
|
+
}
|
|
181
|
+
if (current && Object.entries(entry).every(([field, value]) => sameJson(current[field], value))) {
|
|
182
|
+
return { action: "unchanged" };
|
|
183
|
+
}
|
|
184
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
185
|
+
const indent = /^([ \t]+)\S/m.exec(existing)?.[1] ?? " ";
|
|
186
|
+
const formattingOptions = {
|
|
187
|
+
insertSpaces: !indent.startsWith("\t"),
|
|
188
|
+
tabSize: indent.startsWith("\t") ? 1 : indent.length,
|
|
189
|
+
eol,
|
|
190
|
+
};
|
|
191
|
+
let content = existing;
|
|
192
|
+
// Field by field when the entry exists, so its other fields and any
|
|
193
|
+
// comments inside it stay; whole when it does not.
|
|
194
|
+
const edits = current
|
|
195
|
+
? Object.entries(entry).map(([field, value]) => [[key, "4da", field], value])
|
|
196
|
+
: [[[key, "4da"], entry]];
|
|
197
|
+
for (const [jsonPath, value] of edits) {
|
|
198
|
+
content = jsonc.applyEdits(content, jsonc.modify(content, jsonPath, value, { formattingOptions }));
|
|
199
|
+
}
|
|
200
|
+
return { action: "update", content };
|
|
201
|
+
}
|
|
202
|
+
/** Read a config file's text; null when it does not exist. */
|
|
203
|
+
function readConfig(filePath) {
|
|
204
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf-8") : null;
|
|
205
|
+
}
|
|
206
|
+
/** `<file>.bak`, or a timestamped name when that is taken: an earlier backup is never overwritten. */
|
|
207
|
+
function backupPath(filePath) {
|
|
208
|
+
const plain = `${filePath}.bak`;
|
|
209
|
+
if (!existsSync(plain))
|
|
210
|
+
return plain;
|
|
211
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..*$/, "");
|
|
212
|
+
for (let n = 0;; n++) {
|
|
213
|
+
const candidate = `${filePath}.${stamp}${n === 0 ? "" : `-${n}`}.bak`;
|
|
214
|
+
if (!existsSync(candidate))
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Write `content` to `filePath`: back up an existing file first, then write a
|
|
220
|
+
* temp file beside it and rename it over the original, so an interrupted run
|
|
221
|
+
* leaves either the old file or the new one, never half of one. A symlinked
|
|
222
|
+
* config (a dotfiles repo) is written through to its target.
|
|
223
|
+
* @returns The backup path, when one was made.
|
|
224
|
+
*/
|
|
225
|
+
export function writeConfigAtomic(filePath, content) {
|
|
226
|
+
// Only a symlinked file is resolved: resolving the whole path would turn
|
|
227
|
+
// macOS's /var/... into /private/var/... in every path reported back.
|
|
228
|
+
const target = existsSync(filePath) && lstatSync(filePath).isSymbolicLink() ? realpathSync(filePath) : filePath;
|
|
229
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
230
|
+
let backup = null;
|
|
231
|
+
if (existsSync(target)) {
|
|
232
|
+
backup = backupPath(target);
|
|
233
|
+
copyFileSync(target, backup);
|
|
234
|
+
}
|
|
235
|
+
const temp = `${target}.4da-${process.pid}.tmp`;
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(temp, content);
|
|
238
|
+
renameSync(temp, target);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
rmSync(temp, { force: true });
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
return backup;
|
|
245
|
+
}
|
|
246
|
+
/** The snippet an editor gets, as printed in a dry run and for manual setup. */
|
|
247
|
+
function snippet(target) {
|
|
248
|
+
return JSON.stringify({ [target.key]: { "4da": target.entry } }, null, 2)
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((line) => ` ${line}`)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
/** Whether `~/.claude.json` already has a user-scope `4da` server. Read-only. */
|
|
254
|
+
function claudeCodeHas4da(configPath) {
|
|
255
|
+
try {
|
|
256
|
+
const root = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
257
|
+
return isPlainObject(root) && isPlainObject(root.mcpServers) && "4da" in root.mcpServers;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
128
263
|
/**
|
|
129
264
|
* Run the setup wizard: detect editors and write MCP configurations.
|
|
130
265
|
* @param dryRun If true, only report what would be written without modifying files.
|
|
131
266
|
*/
|
|
132
|
-
export function runSetup(dryRun = false) {
|
|
133
|
-
|
|
267
|
+
export function runSetup(dryRun = false, ctx = currentContext(), log = console.log) {
|
|
268
|
+
log("\n 4DA MCP Server — Setup\n");
|
|
134
269
|
if (dryRun)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
270
|
+
log(" [DRY RUN] No files will be modified.\n");
|
|
271
|
+
log(" Detecting editors...\n");
|
|
272
|
+
let found = 0;
|
|
273
|
+
let refused = 0;
|
|
274
|
+
// Claude Code: print the command, never edit ~/.claude.json.
|
|
275
|
+
const claudeJson = claudeCodeConfigPath(ctx);
|
|
276
|
+
const claudeDir = pathFor(ctx).join(ctx.home, ".claude");
|
|
277
|
+
if (existsSync(claudeJson) || existsSync(claudeDir)) {
|
|
278
|
+
found++;
|
|
279
|
+
log(" Found: Claude Code");
|
|
280
|
+
if (claudeCodeHas4da(claudeJson)) {
|
|
281
|
+
log(` Already configured (user scope, ${claudeJson}).`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
log(" Claude Code keeps its MCP servers in ~/.claude.json, which it rewrites");
|
|
285
|
+
log(" itself; setup does not edit it. Add the server with:");
|
|
286
|
+
log(` ${CLAUDE_CODE_COMMAND}`);
|
|
287
|
+
}
|
|
288
|
+
log("");
|
|
289
|
+
}
|
|
290
|
+
for (const target of editorTargets(ctx)) {
|
|
291
|
+
if (!existsSync(target.detectDir))
|
|
292
|
+
continue;
|
|
293
|
+
found++;
|
|
294
|
+
log(` Found: ${target.name}`);
|
|
295
|
+
let plan;
|
|
296
|
+
try {
|
|
297
|
+
plan = planConfig(readConfig(target.configPath), target.key, target.entry);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
plan = { action: "refuse", reason: `it could not be read (${error instanceof Error ? error.message : String(error)})` };
|
|
301
|
+
}
|
|
302
|
+
if (plan.action === "unchanged") {
|
|
303
|
+
log(` Already configured: ${target.configPath}`);
|
|
304
|
+
}
|
|
305
|
+
else if (plan.action === "refuse") {
|
|
306
|
+
refused++;
|
|
307
|
+
log(` Not modified: ${target.configPath}`);
|
|
308
|
+
log(` Setup left it untouched because ${plan.reason}. Add this by hand:`);
|
|
309
|
+
log(snippet(target));
|
|
310
|
+
}
|
|
311
|
+
else if (dryRun) {
|
|
312
|
+
log(` Would ${plan.action === "create" ? "create" : "update"}: ${target.configPath}`);
|
|
313
|
+
log(` Adding under "${target.key}":`);
|
|
314
|
+
log(snippet(target));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
try {
|
|
318
|
+
const backup = writeConfigAtomic(target.configPath, plan.content ?? "");
|
|
319
|
+
log(` Written: ${target.configPath}`);
|
|
320
|
+
if (backup)
|
|
321
|
+
log(` Backup: ${backup}`);
|
|
144
322
|
}
|
|
145
|
-
|
|
146
|
-
|
|
323
|
+
catch (error) {
|
|
324
|
+
refused++;
|
|
325
|
+
log(` Could not write ${target.configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
|
+
log(" Add this by hand:");
|
|
327
|
+
log(snippet(target));
|
|
147
328
|
}
|
|
148
|
-
configured++;
|
|
149
|
-
console.log("");
|
|
150
329
|
}
|
|
330
|
+
log("");
|
|
331
|
+
}
|
|
332
|
+
if (found === 0) {
|
|
333
|
+
log(" No supported editors detected.\n");
|
|
334
|
+
log(" Manual setup — add to your editor's MCP config:\n");
|
|
335
|
+
log(` ${JSON.stringify({ mcpServers: MCP_CONFIG }, null, 2)}\n`);
|
|
336
|
+
log(` Claude Code: ${CLAUDE_CODE_COMMAND}\n`);
|
|
151
337
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.log(" https://github.com/4DA-Systems/4DA/releases/latest");
|
|
162
|
-
console.log("");
|
|
163
|
-
console.log(" Run: npx @4da/mcp-server --doctor to verify everything works.\n");
|
|
338
|
+
if (refused > 0)
|
|
339
|
+
log(` ${refused} config file${refused === 1 ? " needs" : "s need"} the entry added by hand (above).`);
|
|
340
|
+
log(" Done. Restart your editor to activate 4DA.");
|
|
341
|
+
log("");
|
|
342
|
+
log(" The server works on its own; with the 4DA desktop app installed it");
|
|
343
|
+
log(" also reads the app's scored feed:");
|
|
344
|
+
log(" https://github.com/4DA-Systems/4DA/releases/latest");
|
|
345
|
+
log("");
|
|
346
|
+
log(" Run: npx @4da/mcp-server --doctor to verify everything works.\n");
|
|
164
347
|
}
|
|
165
348
|
// Run directly if invoked as a script (bin entry or pnpm run setup)
|
|
166
349
|
const isDirectRun = process.argv[1]?.endsWith("setup.js") || process.argv[1]?.endsWith("setup.ts");
|
package/dist/tool-dispatch.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type ToolExecutor = (db: FourDADatabase, params: any) => unknown | Promis
|
|
|
20
20
|
* context; the host already owns the process).
|
|
21
21
|
*/
|
|
22
22
|
export declare function dispatchTool(name: string, db: FourDADatabase, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
|
|
23
|
+
/** What a desktop-only tool says when no desktop app database was found. */
|
|
24
|
+
export declare function desktopAppNote(name: string): string;
|
|
23
25
|
/** Check if a tool exists in the dispatch map */
|
|
24
26
|
export declare function hasDispatchTool(name: string): boolean;
|
|
25
27
|
/** Get count of registered tool executors */
|
package/dist/tool-dispatch.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { assertToolPermission } from "./auth-context.js";
|
|
9
9
|
import { checkBuildStaleness } from "./build-staleness.js";
|
|
10
10
|
import { cleanStrings } from "./tool-args.js";
|
|
11
|
+
import { TOOL_REGISTRY } from "./schema-registry.js";
|
|
11
12
|
import { executeGetRelevantContent, executeGetContext, executeRecordFeedback, executeGetActionableSignals, executeKnowledgeGaps, executeDecisionMemory, executeCheckDecisionAlignment, executeAgentMemory, executeDeveloperDna, executeWhatShouldIKnow, executeVulnerabilityScan, executeEcosystemPulse, executeDependencyHealth, executeUpgradePlanner, executeDependencyCheck, executeUpgradeImpact, } from "./tools/index.js";
|
|
12
13
|
import { getLiveIntelligence } from "./live-singleton.js";
|
|
13
14
|
/**
|
|
@@ -86,7 +87,11 @@ export async function dispatchTool(name, db, args) {
|
|
|
86
87
|
// Feed freshness describes the desktop app's feed; a standalone install has
|
|
87
88
|
// none, and telling it to "run fourda-engine" is noise.
|
|
88
89
|
const withFreshness = FRESHNESS_TOOLS.has(name) && !db.isStandalone;
|
|
89
|
-
|
|
90
|
+
// A desktop-only tool called without the desktop app's database (tools/list
|
|
91
|
+
// hides these in standalone mode, but a host can call one by name) used to
|
|
92
|
+
// answer `[]`, which reads as "nothing relevant" rather than "no app".
|
|
93
|
+
const desktopOnly = db.isStandalone && TOOL_REGISTRY[name]?.standalone === false;
|
|
94
|
+
const payload = cleanStrings(withFreshness ? attachFreshness(db, result) : desktopOnly ? attachDesktopAppNote(name, result) : result);
|
|
90
95
|
// An executor that returns `{ error: "<message>" }` failed: say so in the
|
|
91
96
|
// protocol, not only in the body, so the host and model treat it as an error.
|
|
92
97
|
const failed = payload !== null && typeof payload === "object" && typeof payload.error === "string";
|
|
@@ -140,6 +145,21 @@ function attachFreshness(db, result) {
|
|
|
140
145
|
}
|
|
141
146
|
return { data_freshness, ...extras, result };
|
|
142
147
|
}
|
|
148
|
+
/** What a desktop-only tool says when no desktop app database was found. */
|
|
149
|
+
export function desktopAppNote(name) {
|
|
150
|
+
return (`${name} reads the 4DA desktop app's database, and none was found (standalone mode), so this result is empty or partial. ` +
|
|
151
|
+
"Install the app (https://github.com/4DA-Systems/4DA/releases/latest) or set FOURDA_DB_PATH to its database. " +
|
|
152
|
+
"The dependency, vulnerability, upgrade and decision tools work without it.");
|
|
153
|
+
}
|
|
154
|
+
/** Same wrapping as attachFreshness: an array becomes { desktop_app_note, item_count, items }. */
|
|
155
|
+
function attachDesktopAppNote(name, result) {
|
|
156
|
+
const desktop_app_note = desktopAppNote(name);
|
|
157
|
+
if (Array.isArray(result))
|
|
158
|
+
return { desktop_app_note, item_count: result.length, items: result };
|
|
159
|
+
if (result && typeof result === "object")
|
|
160
|
+
return { desktop_app_note, ...result };
|
|
161
|
+
return { desktop_app_note, result };
|
|
162
|
+
}
|
|
143
163
|
/** Check if a tool exists in the dispatch map */
|
|
144
164
|
export function hasDispatchTool(name) {
|
|
145
165
|
return name in DISPATCH_MAP;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4da/mcp-server",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"mcpName": "io.github.4DA-Systems/4da-mcp-server",
|
|
5
5
|
"description": "Upgrade intelligence for AI coding agents: what changes between your installed and target version (changelog, breaking changes, the files you import it from), a pre-install verdict on each bump, vulnerability scanning at osv-scanner parity across npm/pnpm/yarn, Cargo, Python and Go lockfiles, ranked upgrade plans, task-scoped briefings, decision memory. Local, no account; works standalone, and reads the 4DA desktop app's scored feed when it is installed.",
|
|
6
6
|
"type": "module",
|
|
@@ -81,16 +81,17 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
84
|
-
"@modelcontextprotocol/server": "^2.
|
|
84
|
+
"@modelcontextprotocol/server": "^2.2.0",
|
|
85
85
|
"better-sqlite3": "^12.9.0",
|
|
86
|
-
"hono": "^4.12
|
|
86
|
+
"hono": "^4.13.12",
|
|
87
|
+
"jsonc-parser": "^3.3.1"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@types/better-sqlite3": "^7.6.11",
|
|
90
|
-
"@types/node": "^26.
|
|
91
|
+
"@types/node": "^26.6.3",
|
|
91
92
|
"typescript": "^7.0.2",
|
|
92
93
|
"vite": "~7.3.5",
|
|
93
|
-
"vitest": "^5.0.
|
|
94
|
+
"vitest": "^5.0.3"
|
|
94
95
|
},
|
|
95
96
|
"engines": {
|
|
96
97
|
"node": ">=22.0.0"
|