@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 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) {
@@ -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 */
@@ -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 payload = cleanStrings(withFreshness ? attachFreshness(db, result) : desktopOnly ? attachDesktopAppNote(name, result) : result);
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 ? ORDER BY created_at DESC LIMIT 50`)
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.0",
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",