@4da/mcp-server 6.2.0 → 6.2.1
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 +15 -0
- package/dist/db.d.ts +15 -0
- package/dist/db.js +37 -2
- package/dist/tool-dispatch.d.ts +2 -0
- package/dist/tool-dispatch.js +41 -1
- package/dist/tools/briefing-stored-advisories.js +1 -1
- package/dist/tools/briefing-task-scope.js +2 -2
- package/dist/tools/knowledge-gap-citations.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.2.1 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
### Feed tools leave out sources you turned off in the 4DA app
|
|
6
|
+
|
|
7
|
+
Since app schema 126, social and editorial sources (Hacker News, Reddit, RSS
|
|
8
|
+
and the rest) are opt-in "interests", off until turned on in Settings →
|
|
9
|
+
Sources, and every app reader skips their items. The server still returned
|
|
10
|
+
them. `get_relevant_content`, `get_actionable_signals`, `knowledge_gaps` and
|
|
11
|
+
`what_should_i_know` now apply the app's own rule
|
|
12
|
+
(`source_type NOT IN (SELECT source_type FROM sources WHERE enabled = 0)`), and
|
|
13
|
+
say so in a `sources_note` when any source is off. A source with no `sources`
|
|
14
|
+
row counts as on, as in the app; a database without `sources.enabled` is read
|
|
15
|
+
unfiltered, as before. `developer_dna` is unchanged: like the app's own, it
|
|
16
|
+
reports reading history. The vendored app-schema contract moves to schema 127.
|
|
17
|
+
|
|
3
18
|
## 6.2.0 — 2026-10-11
|
|
4
19
|
|
|
5
20
|
### `dependency_check`: the release-age hold no longer leaves only vulnerable choices
|
package/dist/db.d.ts
CHANGED
|
@@ -257,6 +257,21 @@ export declare class FourDADatabase {
|
|
|
257
257
|
/** `MAX(sources.last_fetch)`, tolerant of a missing `sources` table (returns null). */
|
|
258
258
|
private safeMaxLastFetch;
|
|
259
259
|
hasTable(table: string): boolean;
|
|
260
|
+
/** Whether `sources.enabled` exists; probed once per connection (see enabledSourceClause). */
|
|
261
|
+
private sourcesEnabledColumn;
|
|
262
|
+
private hasSourcesEnabled;
|
|
263
|
+
/**
|
|
264
|
+
* ` AND <col> is a source the user has not turned off`, or `` when the
|
|
265
|
+
* database cannot say. The desktop app's rule, verbatim
|
|
266
|
+
* (`source_class::enabled_source_sql`, schema 126): every reader of source
|
|
267
|
+
* items skips a `source_type` whose `sources` row has `enabled = 0`. A type
|
|
268
|
+
* with no `sources` row counts as on, as in the app. A database without a
|
|
269
|
+
* `sources.enabled` column (or without `sources`) is read unfiltered, exactly
|
|
270
|
+
* as before this guard existed.
|
|
271
|
+
*/
|
|
272
|
+
enabledSourceClause(col: string): string;
|
|
273
|
+
/** Source types the user turned off in the app (`sources.enabled = 0`), sorted; [] when unknown. */
|
|
274
|
+
disabledSources(): string[];
|
|
260
275
|
hasColumn(table: string, column: string): boolean;
|
|
261
276
|
/**
|
|
262
277
|
* Idempotently add a column to a table if it does not already exist.
|
package/dist/db.js
CHANGED
|
@@ -1033,6 +1033,41 @@ export class FourDADatabase {
|
|
|
1033
1033
|
return false;
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
|
+
/** Whether `sources.enabled` exists; probed once per connection (see enabledSourceClause). */
|
|
1037
|
+
sourcesEnabledColumn;
|
|
1038
|
+
hasSourcesEnabled() {
|
|
1039
|
+
if (this.sourcesEnabledColumn === undefined) {
|
|
1040
|
+
this.sourcesEnabledColumn = this.hasColumn("sources", "enabled");
|
|
1041
|
+
}
|
|
1042
|
+
return this.sourcesEnabledColumn;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* ` AND <col> is a source the user has not turned off`, or `` when the
|
|
1046
|
+
* database cannot say. The desktop app's rule, verbatim
|
|
1047
|
+
* (`source_class::enabled_source_sql`, schema 126): every reader of source
|
|
1048
|
+
* items skips a `source_type` whose `sources` row has `enabled = 0`. A type
|
|
1049
|
+
* with no `sources` row counts as on, as in the app. A database without a
|
|
1050
|
+
* `sources.enabled` column (or without `sources`) is read unfiltered, exactly
|
|
1051
|
+
* as before this guard existed.
|
|
1052
|
+
*/
|
|
1053
|
+
enabledSourceClause(col) {
|
|
1054
|
+
return this.hasSourcesEnabled()
|
|
1055
|
+
? ` AND ${col} NOT IN (SELECT source_type FROM sources WHERE enabled = 0)`
|
|
1056
|
+
: ``;
|
|
1057
|
+
}
|
|
1058
|
+
/** Source types the user turned off in the app (`sources.enabled = 0`), sorted; [] when unknown. */
|
|
1059
|
+
disabledSources() {
|
|
1060
|
+
if (!this.hasSourcesEnabled())
|
|
1061
|
+
return [];
|
|
1062
|
+
try {
|
|
1063
|
+
return this.db
|
|
1064
|
+
.prepare("SELECT source_type FROM sources WHERE enabled = 0 ORDER BY source_type")
|
|
1065
|
+
.all().map((r) => r.source_type);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1036
1071
|
hasColumn(table, column) {
|
|
1037
1072
|
try {
|
|
1038
1073
|
const cols = this.db.pragma(`table_info(${table})`);
|
|
@@ -1129,7 +1164,7 @@ export class FourDADatabase {
|
|
|
1129
1164
|
${depCount} AS dep_match_count
|
|
1130
1165
|
FROM source_items
|
|
1131
1166
|
WHERE relevance_score >= ?
|
|
1132
|
-
AND datetime(created_at) >= datetime(?)${versionGuard}${curationGuard}${advisoryFreshnessGuard}
|
|
1167
|
+
AND datetime(created_at) >= datetime(?)${versionGuard}${curationGuard}${advisoryFreshnessGuard}${this.enabledSourceClause("source_type")}
|
|
1133
1168
|
`;
|
|
1134
1169
|
const params = [minScore, sinceDate];
|
|
1135
1170
|
if (sourceType) {
|
|
@@ -1213,7 +1248,7 @@ export class FourDADatabase {
|
|
|
1213
1248
|
let query = `
|
|
1214
1249
|
SELECT id, source_type, source_id, url, title, content, content_hash, created_at, last_seen
|
|
1215
1250
|
FROM source_items
|
|
1216
|
-
WHERE datetime(created_at) >= datetime(?)${advisoryFreshnessGuard}
|
|
1251
|
+
WHERE datetime(created_at) >= datetime(?)${advisoryFreshnessGuard}${this.enabledSourceClause("source_type")}
|
|
1217
1252
|
`;
|
|
1218
1253
|
const params = [sinceDate];
|
|
1219
1254
|
if (sourceType) {
|
package/dist/tool-dispatch.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare const DATABASE_FREE_TOOLS: ReadonlySet<string>;
|
|
|
27
27
|
* context; the host already owns the process).
|
|
28
28
|
*/
|
|
29
29
|
export declare function dispatchTool(name: string, db: FourDADatabase | null, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
|
|
30
|
+
/** The sentence `sources_note` carries, or null when no source is turned off. */
|
|
31
|
+
export declare function sourcesNote(disabled: string[]): string | null;
|
|
30
32
|
/** What a desktop-only tool says when no desktop app database was found. */
|
|
31
33
|
export declare function desktopAppNote(name: string): string;
|
|
32
34
|
/** Check if a tool exists in the dispatch map */
|
package/dist/tool-dispatch.js
CHANGED
|
@@ -106,7 +106,8 @@ export async function dispatchTool(name, db, args) {
|
|
|
106
106
|
// hides these in standalone mode, but a host can call one by name) used to
|
|
107
107
|
// answer `[]`, which reads as "nothing relevant" rather than "no app".
|
|
108
108
|
const desktopOnly = db !== null && db.isStandalone && TOOL_REGISTRY[name]?.standalone === false;
|
|
109
|
-
const
|
|
109
|
+
const annotated = withFreshness ? attachFreshness(db, result) : desktopOnly ? attachDesktopAppNote(name, result) : result;
|
|
110
|
+
const payload = cleanStrings(db !== null && !db.isStandalone && SOURCE_FILTERED_TOOLS.has(name) ? attachSourcesNote(db, annotated) : annotated);
|
|
110
111
|
// An executor that returns `{ error: "<message>" }` failed: say so in the
|
|
111
112
|
// protocol, not only in the body, so the host and model treat it as an error.
|
|
112
113
|
const failed = payload !== null && typeof payload === "object" && typeof payload.error === "string";
|
|
@@ -160,6 +161,45 @@ function attachFreshness(db, result) {
|
|
|
160
161
|
}
|
|
161
162
|
return { data_freshness, ...extras, result };
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Tools that read the app's feed items and therefore skip items from sources
|
|
166
|
+
* the user turned off in the app (`FourDADatabase.enabledSourceClause`).
|
|
167
|
+
* developer_dna is not here: like the app's own developer_dna.rs it reports
|
|
168
|
+
* reading history, unfiltered, and returns no items.
|
|
169
|
+
*/
|
|
170
|
+
const SOURCE_FILTERED_TOOLS = new Set([
|
|
171
|
+
"get_relevant_content",
|
|
172
|
+
"get_actionable_signals",
|
|
173
|
+
"knowledge_gaps",
|
|
174
|
+
"what_should_i_know",
|
|
175
|
+
]);
|
|
176
|
+
/** The desktop app's always-on source types (`source_class::STACK_SOURCES`); every other type is an opt-in interest. */
|
|
177
|
+
const STACK_SOURCES = new Set(["crates_io", "npm_registry", "pypi", "go_modules", "osv", "cve"]);
|
|
178
|
+
/** The sentence `sources_note` carries, or null when no source is turned off. */
|
|
179
|
+
export function sourcesNote(disabled) {
|
|
180
|
+
if (disabled.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
const kind = disabled.every((s) => !STACK_SOURCES.has(s)) ? "interest " : "";
|
|
183
|
+
const subject = disabled.length === 1 ? `1 ${kind}source is` : `${disabled.length} ${kind}sources are`;
|
|
184
|
+
return `${subject} turned off in the 4DA app; their items are not included. Turn them on in Settings → Sources.`;
|
|
185
|
+
}
|
|
186
|
+
/** Say, on the payload, that items from turned-off sources were left out. Best-effort, like attachFreshness. */
|
|
187
|
+
function attachSourcesNote(db, result) {
|
|
188
|
+
let sources_note;
|
|
189
|
+
try {
|
|
190
|
+
sources_note = sourcesNote(db.disabledSources());
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
if (!sources_note)
|
|
196
|
+
return result;
|
|
197
|
+
if (Array.isArray(result))
|
|
198
|
+
return { sources_note, item_count: result.length, items: result };
|
|
199
|
+
if (result && typeof result === "object")
|
|
200
|
+
return { sources_note, ...result };
|
|
201
|
+
return { sources_note, result };
|
|
202
|
+
}
|
|
163
203
|
/** What a desktop-only tool says when no desktop app database was found. */
|
|
164
204
|
export function desktopAppNote(name) {
|
|
165
205
|
return (`${name} reads the 4DA desktop app's database, and none was found (standalone mode), so this result is empty or partial. ` +
|
|
@@ -33,7 +33,7 @@ export function storedAdvisoriesFor(db, packages, limit = 5) {
|
|
|
33
33
|
WHERE source_type IN (${ADVISORY_SOURCES.map(() => "?").join(", ")})
|
|
34
34
|
AND created_at > datetime('now', '-90 days')
|
|
35
35
|
AND LOWER(title) LIKE ?
|
|
36
|
-
${hasFeedRelevant ? "AND (feed_relevant IS NULL OR feed_relevant = 1)" : ""}
|
|
36
|
+
${hasFeedRelevant ? "AND (feed_relevant IS NULL OR feed_relevant = 1)" : ""}${db.enabledSourceClause("source_type")}
|
|
37
37
|
ORDER BY created_at DESC LIMIT 20`)
|
|
38
38
|
.all(...ADVISORY_SOURCES, `%${pkg.name.toLowerCase()}%`);
|
|
39
39
|
}
|
|
@@ -226,7 +226,7 @@ export function releasesFor(db, pkg) {
|
|
|
226
226
|
rows = db
|
|
227
227
|
.getRawDb()
|
|
228
228
|
.prepare(`SELECT title, url, created_at FROM source_items
|
|
229
|
-
WHERE source_type = ? AND title LIKE
|
|
229
|
+
WHERE source_type = ? AND title LIKE ?${db.enabledSourceClause("source_type")} ORDER BY created_at DESC LIMIT 50`)
|
|
230
230
|
.all(spec.source, `${spec.prefix}${pkg.name} v%`);
|
|
231
231
|
}
|
|
232
232
|
catch {
|
|
@@ -266,7 +266,7 @@ export function judgedReadingFor(db, packages, limit = 5) {
|
|
|
266
266
|
SELECT s.id, s.title, s.url, s.source_type, l.relevance_score AS verdict, s.created_at
|
|
267
267
|
FROM source_items s JOIN latest l ON l.source_item_id = s.id AND l.rn = 1
|
|
268
268
|
WHERE s.feed_relevant = 1 AND l.relevance_score >= 0.5
|
|
269
|
-
AND s.created_at > datetime('now', '-30 days')
|
|
269
|
+
AND s.created_at > datetime('now', '-30 days')${db.enabledSourceClause("s.source_type")}
|
|
270
270
|
ORDER BY l.relevance_score DESC, s.created_at DESC LIMIT 2000`)
|
|
271
271
|
.all();
|
|
272
272
|
}
|
|
@@ -56,7 +56,7 @@ export function loadCandidates(db) {
|
|
|
56
56
|
${has("source_id") ? "si.source_id" : "NULL AS source_id"},
|
|
57
57
|
substr(COALESCE(si.content, ''), 1, 2000) AS content_head
|
|
58
58
|
FROM source_items si
|
|
59
|
-
WHERE si.created_at >= datetime('now', '-30 days')
|
|
59
|
+
WHERE si.created_at >= datetime('now', '-30 days')${db.enabledSourceClause("si.source_type")}
|
|
60
60
|
${hasRelevance ? "AND si.relevance_score IS NOT NULL AND si.relevance_score >= 0.2" : ""}
|
|
61
61
|
${hasContentType ? `AND (si.content_type IS NULL OR si.content_type NOT IN (${excludedTypes}))` : ""}
|
|
62
62
|
${engaged}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4da/mcp-server",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.1",
|
|
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 adds the 4DA desktop app's signals, knowledge gaps and stack-judged news when it is installed.",
|
|
6
6
|
"type": "module",
|