@4da/mcp-server 4.6.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@4da/mcp-server?color=gold)](https://www.npmjs.com/package/@4da/mcp-server)
4
4
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
5
- [![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
+ [![Node](https://img.shields.io/badge/Node-%3E%3D20-brightgreen)](https://nodejs.org/)
6
6
  [![smithery badge](https://smithery.ai/badge/@4da/mcp-server)](https://smithery.ai/server/@4da/mcp-server)
7
7
 
8
8
  **Dependency intelligence for AI coding agents.** Live CVE scanning, dependency health checks, upgrade planning, ecosystem news, and persistent decision memory. Zero config, privacy-first.
package/dist/db.d.ts CHANGED
@@ -107,7 +107,7 @@ export declare class FourDADatabase {
107
107
  * Get relevant content items scored by the Rust pipeline (preferred) or
108
108
  * TypeScript fallback scoring when Rust scores are unavailable.
109
109
  */
110
- getRelevantContent(minScore?: number, sourceType?: string, limit?: number, sinceHours?: number): RelevantItem[];
110
+ getRelevantContent(minScore?: number, sourceType?: string, limit?: number, sinceHours?: number, requireCurrentVersion?: boolean): RelevantItem[];
111
111
  /**
112
112
  * Read the live freshness state of the feed database. Answered from ground truth, not from any
113
113
  * claim that a refresh ran — so DB-backed tools can tell the caller whether the curated feed is
package/dist/db.js CHANGED
@@ -635,15 +635,16 @@ export class FourDADatabase {
635
635
  * Get relevant content items scored by the Rust pipeline (preferred) or
636
636
  * TypeScript fallback scoring when Rust scores are unavailable.
637
637
  */
638
- getRelevantContent(minScore = 0.35, sourceType, limit = 20, sinceHours = 24) {
638
+ getRelevantContent(minScore = 0.35, sourceType, limit = 20, sinceHours = 24, requireCurrentVersion = false) {
639
639
  const sinceDate = new Date(Date.now() - sinceHours * 60 * 60 * 1000)
640
640
  .toISOString()
641
641
  .replace("T", " ")
642
642
  .slice(0, 19);
643
643
  const hasRustScores = this.hasColumn("source_items", "relevance_score");
644
644
  if (hasRustScores) {
645
- return this.getRelevantContentFromRust(minScore, sourceType, limit, sinceDate);
645
+ return this.getRelevantContentFromRust(minScore, sourceType, limit, sinceDate, requireCurrentVersion);
646
646
  }
647
+ // TypeScript fallback computes scores fresh per call — current by construction.
647
648
  return this.getRelevantContentFallback(minScore, sourceType, limit, sinceDate);
648
649
  }
649
650
  /**
@@ -722,13 +723,57 @@ export class FourDADatabase {
722
723
  * Use Rust-computed relevance_score from the desktop app's scoring pipeline.
723
724
  * These scores are from the full 5-axis PASIFA engine — much more accurate.
724
725
  */
725
- getRelevantContentFromRust(minScore, sourceType, limit, sinceDate) {
726
+ getRelevantContentFromRust(minScore, sourceType, limit, sinceDate, requireCurrentVersion = false) {
726
727
  // Grounding subquery — guarded: older/partial 4DA DBs may have relevance_score
727
728
  // (so the Rust path runs) but lack source_item_dependencies. Fall back to 0
728
729
  // (→ evidence_class "semantic_only") rather than throwing.
729
730
  const depCount = this.hasColumn("source_item_dependencies", "source_item_id")
730
731
  ? `(SELECT COUNT(*) FROM source_item_dependencies d WHERE d.source_item_id = source_items.id)`
731
732
  : `0`;
733
+ // Stale-epoch guard for deep-window fallbacks: when the caller widens to the
734
+ // 30-day window it reaches items whose stored score/signal columns may come
735
+ // from an OLDER scoring pipeline (a version bump makes the tail stale until
736
+ // the drain re-scores it). Ranking those raw scores against current ones —
737
+ // or trusting stale signal_type/signal_priority — produces claims the
738
+ // current pipeline doesn't stand behind. The MCP server can't import the
739
+ // Rust PIPELINE_VERSION constant, so the DB itself is the source of truth:
740
+ // MAX(scored_pipeline_version) IS the current brain's stamp (uncorrelated
741
+ // subquery, computed once per statement). Guarded on column existence for
742
+ // older desktop-app DBs.
743
+ const versionGuard = requireCurrentVersion && this.hasColumn("source_items", "scored_pipeline_version")
744
+ ? ` AND scored_pipeline_version = (SELECT MAX(scored_pipeline_version) FROM source_items)`
745
+ : ``;
746
+ // Curation guard: never return an item the current pipeline EXPLICITLY
747
+ // rejected. `feed_relevant` is the analysis run's persisted verdict, and a
748
+ // score threshold alone does not respect it — the desktop content graph hit
749
+ // exactly this and adopted corpus parity in Phase 95 (W4-5).
750
+ //
751
+ // It matters because a verdict can be negative while the score stays above
752
+ // the threshold. v18's look-alike gate makes ungrounded registry releases
753
+ // categorically non-relevant while deliberately KEEPING their capped score
754
+ // (0.37 / 0.42) for ranking and display, so `relevance_score >= 0.35` alone
755
+ // still surfaces them. Live 2026-07-26: 30 crates_io items inside the
756
+ // 30-day window were already `feed_relevant = 0` and were still returned.
757
+ //
758
+ // NULL is KEPT, not excluded: an unjudged item is "not yet curated", not
759
+ // "rejected", and dropping it would blank the tool on a fresh corpus
760
+ // (cold-start doctrine). Guarded on column existence for older 4DA DBs.
761
+ //
762
+ // Deliberately NOT extended with `feed_verdict_version = <current>` even
763
+ // though Phase 101 added that stamp, and even though the 720h deep-fallback
764
+ // above DOES gate on `scored_pipeline_version`. The asymmetry is intentional:
765
+ // the version guard there narrows a last-resort widening, whereas this guard
766
+ // sits on the PRIMARY path, so excluding not-yet-reconciled verdicts would
767
+ // empty the tool after every pipeline bump until the reconciliation pass
768
+ // caught up (measured on the desktop: 94% of live graph nodes held a stale
769
+ // verdict immediately after the v18 bump). The reconciliation converging is
770
+ // the fix — see `analysis_backfill::reconcile_stale_verdicts_cycle`. This
771
+ // note exists because `check-mcp-server-sync.cjs` asks "do MCP queries need
772
+ // updating?" on every db-layer change; the answer for the verdict stamp is
773
+ // no, by design.
774
+ const curationGuard = this.hasColumn("source_items", "feed_relevant")
775
+ ? ` AND (feed_relevant IS NULL OR feed_relevant = 1)`
776
+ : ``;
732
777
  let query = `
733
778
  SELECT id, source_type, source_id, url, title, content, content_hash,
734
779
  created_at, last_seen, relevance_score, content_type,
@@ -736,7 +781,7 @@ export class FourDADatabase {
736
781
  ${depCount} AS dep_match_count
737
782
  FROM source_items
738
783
  WHERE relevance_score >= ?
739
- AND datetime(created_at) >= datetime(?)
784
+ AND datetime(created_at) >= datetime(?)${versionGuard}${curationGuard}
740
785
  `;
741
786
  const params = [minScore, sinceDate];
742
787
  if (sourceType) {
@@ -1008,12 +1053,13 @@ export class FourDADatabase {
1008
1053
  context.ace.detected_tech.filter((dt) => itemTopics.some((t) => this.topicMatches(t, dt.name))).length *
1009
1054
  0.05
1010
1055
  : 0;
1011
- const learnedScore = context.learned
1012
- ? context.learned.topic_affinities
1013
- .filter((ta) => ta.affinity_score > 0 && itemTopics.some((t) => this.topicMatches(t, ta.topic)))
1014
- .reduce((sum, ta) => sum + ta.affinity_score * 0.1 * ta.confidence, 0)
1015
- : 0;
1016
- // Anti-penalty from exclusions and anti-topics
1056
+ // Learned affinity score DEMOTED in v19 (AD-029) — must mirror
1057
+ // computeRelevanceScore so the explanation matches what actually
1058
+ // scored (the two previously diverged on the anti-penalty weight too:
1059
+ // scorer used *0.3, this explanation used *0.5).
1060
+ const learnedScore = 0;
1061
+ // Anti-penalty from user-authored exclusions only (learned anti-topics
1062
+ // demoted with the rest of the behavioral stack, AD-029).
1017
1063
  let antiPenalty = 0;
1018
1064
  for (const exclusion of context.exclusions) {
1019
1065
  if (itemTopics.some((t) => this.topicMatches(t, exclusion))) {
@@ -1021,13 +1067,6 @@ export class FourDADatabase {
1021
1067
  break;
1022
1068
  }
1023
1069
  }
1024
- if (antiPenalty === 0 && context.learned) {
1025
- for (const antiTopic of context.learned.anti_topics) {
1026
- if (itemTopics.some((t) => this.topicMatches(t, antiTopic.topic))) {
1027
- antiPenalty = Math.max(antiPenalty, antiTopic.confidence * 0.5);
1028
- }
1029
- }
1030
- }
1031
1070
  const finalScore = Math.max(0, Math.min(1, staticScore + aceScore + learnedScore - antiPenalty));
1032
1071
  const scoreBreakdown = {
1033
1072
  embedding_similarity: null, // Not computing embedding similarity in MCP server
@@ -1087,12 +1126,17 @@ export class FourDADatabase {
1087
1126
  message: `Item ${itemId} of type ${sourceType} not found`,
1088
1127
  };
1089
1128
  }
1090
- // Map action to signal strength
1129
+ // Map action to signal strength — v19: unified onto the canonical ACE
1130
+ // scale (src-tauri/src/ace/behavior/types.rs). The old MCP-only scale
1131
+ // (click 0.3 / save 0.8 / dismiss -0.2 / mark_irrelevant -0.5) meant an
1132
+ // agent-recorded rejection never crossed the Rust side's explicit-
1133
+ // negative (-0.8) or anti-topic (-0.5 exclusive) thresholds, so agent
1134
+ // feedback could never register a real rejection.
1091
1135
  const signalStrength = {
1092
- click: 0.3,
1093
- save: 0.8,
1094
- dismiss: -0.2,
1095
- mark_irrelevant: -0.5,
1136
+ click: 0.55,
1137
+ save: 1.0,
1138
+ dismiss: -0.8,
1139
+ mark_irrelevant: -1.0,
1096
1140
  };
1097
1141
  try {
1098
1142
  const stmt = this.db.prepare(`
@@ -1311,21 +1355,12 @@ export class FourDADatabase {
1311
1355
  }
1312
1356
  }
1313
1357
  }
1314
- // Learned affinities
1315
- if (context.learned) {
1316
- for (const affinity of context.learned.topic_affinities) {
1317
- if (affinity.affinity_score > 0 &&
1318
- itemTopics.some((t) => this.topicMatches(t, affinity.topic))) {
1319
- score += affinity.affinity_score * 0.1 * affinity.confidence;
1320
- }
1321
- }
1322
- // Anti-topics penalty
1323
- for (const antiTopic of context.learned.anti_topics) {
1324
- if (itemTopics.some((t) => this.topicMatches(t, antiTopic.topic))) {
1325
- score -= antiTopic.confidence * 0.3;
1326
- }
1327
- }
1328
- }
1358
+ // Learned affinity/anti-topic weighting DEMOTED in v19 (AD-029) to
1359
+ // mirror the Rust pipeline: this TypeScript fallback scorer applied
1360
+ // its own behavioral weights (+affinity*0.1*confidence,
1361
+ // -anti_confidence*0.3) whenever Rust scores were absent — a second,
1362
+ // uncalibrated ranking regime fed by the same untrusted capture layer.
1363
+ // Static identity + ACE stack matching above remain the scoring basis.
1329
1364
  return Math.max(0, Math.min(1, score));
1330
1365
  }
1331
1366
  /**
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * 4DA MCP Server — Streamable HTTP Transport
3
3
  *
4
- * Implements the MCP Streamable HTTP transport (spec 2025-03-26) using
5
- * Node's built-in http module. No express or other framework needed.
4
+ * Serves MCP over HTTP using the v2 `createMcpHandler` entry (one factory,
5
+ * one endpoint, both protocol eras): 2026-07-28 requests are served natively;
6
+ * 2025-era requests get the established stateless serving (fresh instance per
7
+ * request — the same behavior as the previous per-request
8
+ * `StreamableHTTPServerTransport` wiring). Node's built-in http module only;
9
+ * no express or other framework needed.
6
10
  *
7
11
  * Security:
8
12
  * - Binds to 127.0.0.1 only (localhost)
9
13
  * - DNS rebinding protection via Origin header check
10
- * - Stateless mode (no session tracking)
14
+ * - Stateless (no session tracking)
11
15
  */
12
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { type Server } from "@modelcontextprotocol/server";
13
17
  export interface HttpServerOptions {
14
18
  port: number;
15
19
  host: string;
@@ -17,9 +21,9 @@ export interface HttpServerOptions {
17
21
  /**
18
22
  * Start a Streamable HTTP server for the MCP protocol.
19
23
  *
20
- * Uses stateless mode (sessionIdGenerator: undefined) — each request
21
- * gets its own transport instance. This is the simplest approach and
22
- * is compatible with serverless environments.
24
+ * Takes a server factory: `createMcpHandler` builds a fresh instance per
25
+ * request (stateless — compatible with serverless environments and simple
26
+ * round-robin load balancing).
23
27
  */
24
- export declare function startHttpServer(server: Server, options?: HttpServerOptions): Promise<void>;
28
+ export declare function startHttpServer(factory: () => Server, options?: HttpServerOptions): Promise<void>;
25
29
  //# sourceMappingURL=http-transport.d.ts.map
@@ -2,52 +2,44 @@
2
2
  /**
3
3
  * 4DA MCP Server — Streamable HTTP Transport
4
4
  *
5
- * Implements the MCP Streamable HTTP transport (spec 2025-03-26) using
6
- * Node's built-in http module. No express or other framework needed.
5
+ * Serves MCP over HTTP using the v2 `createMcpHandler` entry (one factory,
6
+ * one endpoint, both protocol eras): 2026-07-28 requests are served natively;
7
+ * 2025-era requests get the established stateless serving (fresh instance per
8
+ * request — the same behavior as the previous per-request
9
+ * `StreamableHTTPServerTransport` wiring). Node's built-in http module only;
10
+ * no express or other framework needed.
7
11
  *
8
12
  * Security:
9
13
  * - Binds to 127.0.0.1 only (localhost)
10
14
  * - DNS rebinding protection via Origin header check
11
- * - Stateless mode (no session tracking)
15
+ * - Stateless (no session tracking)
12
16
  */
13
17
  import { createServer } from "node:http";
14
18
  import { readFileSync } from "node:fs";
15
19
  import { join, dirname } from "node:path";
16
20
  import { fileURLToPath } from "node:url";
17
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21
+ import { createMcpHandler } from "@modelcontextprotocol/server";
22
+ import { toNodeHandler } from "@modelcontextprotocol/node";
18
23
  import { extractAuthClaims } from "./auth.js";
19
24
  const DEFAULT_PORT = 4840;
20
25
  const DEFAULT_HOST = "127.0.0.1";
21
26
  const __ht_dirname = dirname(fileURLToPath(import.meta.url));
22
27
  const SERVER_VERSION = JSON.parse(readFileSync(join(__ht_dirname, "..", "package.json"), "utf-8")).version;
23
- /**
24
- * Parse JSON body from an incoming HTTP request.
25
- */
26
- function parseBody(req) {
27
- return new Promise((resolve, reject) => {
28
- const chunks = [];
29
- req.on("data", (chunk) => chunks.push(chunk));
30
- req.on("end", () => {
31
- try {
32
- const raw = Buffer.concat(chunks).toString("utf-8");
33
- resolve(raw ? JSON.parse(raw) : undefined);
34
- }
35
- catch (e) {
36
- reject(e);
37
- }
38
- });
39
- req.on("error", reject);
40
- });
41
- }
42
28
  /**
43
29
  * Start a Streamable HTTP server for the MCP protocol.
44
30
  *
45
- * Uses stateless mode (sessionIdGenerator: undefined) — each request
46
- * gets its own transport instance. This is the simplest approach and
47
- * is compatible with serverless environments.
31
+ * Takes a server factory: `createMcpHandler` builds a fresh instance per
32
+ * request (stateless — compatible with serverless environments and simple
33
+ * round-robin load balancing).
48
34
  */
49
- export async function startHttpServer(server, options = { port: DEFAULT_PORT, host: DEFAULT_HOST }) {
35
+ export async function startHttpServer(factory, options = { port: DEFAULT_PORT, host: DEFAULT_HOST }) {
50
36
  const { port, host } = options;
37
+ const mcpHandler = createMcpHandler(factory);
38
+ const handleMcpRequest = toNodeHandler(mcpHandler, {
39
+ onerror: (err) => {
40
+ console.error("MCP transport error:", err);
41
+ },
42
+ });
51
43
  const httpServer = createServer(async (req, res) => {
52
44
  // DNS rebinding protection: reject cross-origin requests from non-local origins
53
45
  const origin = req.headers.origin;
@@ -100,36 +92,9 @@ export async function startHttpServer(server, options = { port: DEFAULT_PORT, ho
100
92
  res.end(JSON.stringify({ error: "Not Found" }));
101
93
  return;
102
94
  }
103
- // Parse JSON body for POST requests
104
- let body = undefined;
105
- if (req.method === "POST") {
106
- try {
107
- body = await parseBody(req);
108
- }
109
- catch {
110
- res.writeHead(400, { "Content-Type": "application/json" });
111
- res.end(JSON.stringify({ error: "Invalid JSON body" }));
112
- return;
113
- }
114
- }
115
- // Stateless mode: new transport per request
116
- const transport = new StreamableHTTPServerTransport({
117
- sessionIdGenerator: undefined,
118
- });
119
- res.on("close", () => {
120
- transport.close();
121
- });
122
- try {
123
- await server.connect(transport);
124
- await transport.handleRequest(req, res, body);
125
- }
126
- catch (err) {
127
- console.error("MCP transport error:", err);
128
- if (!res.headersSent) {
129
- res.writeHead(500, { "Content-Type": "application/json" });
130
- res.end(JSON.stringify({ error: "Internal server error" }));
131
- }
132
- }
95
+ // Hand off to the MCP handler — body parsing, era classification, and
96
+ // per-request instance construction happen inside the entry.
97
+ await handleMcpRequest(req, res);
133
98
  });
134
99
  httpServer.listen(port, host, () => {
135
100
  console.error(`4DA MCP Server v${SERVER_VERSION} (Streamable HTTP) listening on http://${host}:${port}/mcp`);
@@ -138,6 +103,7 @@ export async function startHttpServer(server, options = { port: DEFAULT_PORT, ho
138
103
  // Graceful shutdown
139
104
  const shutdown = () => {
140
105
  console.error("Shutting down HTTP server...");
106
+ void mcpHandler.close();
141
107
  httpServer.close();
142
108
  process.exit(0);
143
109
  };
package/dist/index.d.ts CHANGED
@@ -1,29 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/server";
2
3
  /**
3
- * 4DA MCP Server
4
+ * Build a Server instance with every handler registered.
4
5
  *
5
- * Provides dependency-intelligence tools (CVE scanning, dependency health,
6
- * upgrade planning, ecosystem news, decision/agent memory) to Claude Code,
7
- * Claude Desktop, and other MCP hosts. Runs locally; the only data leaving the
8
- * machine is public package names/versions sent to registries (OSV, npm, etc.).
9
- *
10
- * SECURITY: the server trusts its local environment and does NOT implement
11
- * authentication. The optional --http transport is intended for localhost use;
12
- * do NOT expose it over a network without putting your own auth in front.
13
- */
14
- /**
15
- * 4DA MCP Server v4.6.3 — Dependency Intelligence for AI Coding Agents
16
- *
17
- * 14 tools across 5 categories. Live vulnerability scanning (OSV.dev),
18
- * ecosystem news, persistent memory, and tech stack awareness for any MCP host.
19
- *
20
- * Categories (canonical — matches schema-registry.ts `ToolCategory`):
21
- * Security (3) — vulnerability scanning, dependency health, upgrade planning
22
- * Intelligence (7) — briefing, ecosystem pulse, context, content feed,
23
- * actionable signals, knowledge gaps, feedback
24
- * Decisions (2) — decision memory, alignment checking
25
- * Agent (1) — cross-session persistent memory
26
- * Identity (1) — developer DNA profile
6
+ * The v2 serving entries take a factory: `serveStdio` pins one instance per
7
+ * connection; stateless HTTP (`createMcpHandler`) builds one per request.
8
+ * Handlers close over the module-level database singleton, so every instance
9
+ * shares the same lazily-initialized database and live-intelligence layer.
27
10
  */
28
- export {};
11
+ export declare function buildServer(): Server;
29
12
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -11,9 +11,6 @@
11
11
  * SECURITY: the server trusts its local environment and does NOT implement
12
12
  * authentication. The optional --http transport is intended for localhost use;
13
13
  * do NOT expose it over a network without putting your own auth in front.
14
- */
15
- /**
16
- * 4DA MCP Server v4.6.3 — Dependency Intelligence for AI Coding Agents
17
14
  *
18
15
  * 14 tools across 5 categories. Live vulnerability scanning (OSV.dev),
19
16
  * ecosystem news, persistent memory, and tech stack awareness for any MCP host.
@@ -25,10 +22,14 @@
25
22
  * Decisions (2) — decision memory, alignment checking
26
23
  * Agent (1) — cross-session persistent memory
27
24
  * Identity (1) — developer DNA profile
25
+ *
26
+ * Protocol: MCP TypeScript SDK v2. stdio serving goes through `serveStdio`,
27
+ * HTTP through `createMcpHandler` — both entries negotiate the era per
28
+ * connection/request, so 2025-era hosts (classic `initialize` handshake) and
29
+ * 2026-07-28 hosts (stateless `server/discover`) are both supported.
28
30
  */
29
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
30
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
31
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
31
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
32
+ import { Server } from "@modelcontextprotocol/server";
32
33
  import { readFileSync, existsSync } from "node:fs";
33
34
  import { join, dirname } from "node:path";
34
35
  import { fileURLToPath } from "node:url";
@@ -73,15 +74,6 @@ function resolveProjectDir() {
73
74
  return dir;
74
75
  return process.cwd();
75
76
  }
76
- const server = new Server({
77
- name: "4da-server",
78
- version: SERVER_VERSION,
79
- }, {
80
- capabilities: {
81
- tools: { listChanged: true },
82
- resources: {},
83
- },
84
- });
85
77
  // Database instance (lazy initialized)
86
78
  let db = null;
87
79
  let liveIntel = null;
@@ -183,128 +175,136 @@ function getDatabase() {
183
175
  return db;
184
176
  }
185
177
  // =============================================================================
186
- // Tool Handlers
178
+ // Server Factory
187
179
  // =============================================================================
188
180
  /**
189
- * List available tools (SLIM)
190
- *
191
- * Returns one-liner descriptions only (~500 tokens vs ~4500).
192
- * Full schemas available via MCP Resources: 4da://schema/{tool_name}
193
- */
194
- server.setRequestHandler(ListToolsRequestSchema, async () => {
195
- const database = getDatabase();
196
- return {
197
- tools: getSlimToolList(database.isStandalone ? true : undefined),
198
- };
199
- });
200
- /**
201
- * List schema resources
181
+ * Build a Server instance with every handler registered.
202
182
  *
203
- * Exposes full tool schemas as MCP Resources for lazy loading.
204
- * Also exposes skill manifest for agent dispatch.
183
+ * The v2 serving entries take a factory: `serveStdio` pins one instance per
184
+ * connection; stateless HTTP (`createMcpHandler`) builds one per request.
185
+ * Handlers close over the module-level database singleton, so every instance
186
+ * shares the same lazily-initialized database and live-intelligence layer.
205
187
  */
206
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
207
- const resources = [
208
- ...getSchemaResources(),
209
- // Skill manifest for agent dispatch
210
- {
211
- uri: "4da://skills",
212
- name: "Skill manifest",
213
- description: "Registry of 4DA skills for Claude Code agent dispatch",
214
- mimeType: "application/json",
188
+ export function buildServer() {
189
+ const server = new Server({
190
+ name: "4da-server",
191
+ version: SERVER_VERSION,
192
+ }, {
193
+ capabilities: {
194
+ tools: { listChanged: true },
195
+ resources: {},
215
196
  },
216
- // Category manifest for tool discovery
217
- {
218
- uri: "4da://categories",
219
- name: "Tool categories",
220
- description: "Tool groupings by category with tag metadata",
221
- mimeType: "application/json",
222
- },
223
- ];
224
- return { resources };
225
- });
226
- /**
227
- * Read a resource (schema or skill manifest)
228
- */
229
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
230
- const uri = request.params.uri;
231
- // Handle category manifest
232
- if (uri === "4da://categories") {
197
+ });
198
+ // List available tools (SLIM): one-liner descriptions only (~500 tokens vs
199
+ // ~4500). Full schemas available via MCP Resources: 4da://schema/{tool_name}
200
+ server.setRequestHandler("tools/list", async () => {
201
+ const database = getDatabase();
233
202
  return {
234
- contents: [
235
- {
236
- uri,
237
- mimeType: "application/json",
238
- text: JSON.stringify(getCategoryManifest(), null, 2),
239
- },
240
- ],
203
+ tools: getSlimToolList(database.isStandalone ? true : undefined),
241
204
  };
242
- }
243
- // Handle skill manifest
244
- if (uri === "4da://skills") {
245
- const skillsPath = join(homedir(), ".local", "share", "4da", "skills", "registry.json");
246
- if (!existsSync(skillsPath)) {
247
- throw new Error("Skill manifest not found. Run 4DA setup to create it.");
205
+ });
206
+ // List schema resources: full tool schemas as MCP Resources for lazy
207
+ // loading, plus the skill and category manifests.
208
+ server.setRequestHandler("resources/list", async () => {
209
+ const resources = [
210
+ ...getSchemaResources(),
211
+ // Skill manifest for agent dispatch
212
+ {
213
+ uri: "4da://skills",
214
+ name: "Skill manifest",
215
+ description: "Registry of 4DA skills for Claude Code agent dispatch",
216
+ mimeType: "application/json",
217
+ },
218
+ // Category manifest for tool discovery
219
+ {
220
+ uri: "4da://categories",
221
+ name: "Tool categories",
222
+ description: "Tool groupings by category with tag metadata",
223
+ mimeType: "application/json",
224
+ },
225
+ ];
226
+ return { resources };
227
+ });
228
+ // Read a resource (schema or skill manifest)
229
+ server.setRequestHandler("resources/read", async (request) => {
230
+ const uri = request.params.uri;
231
+ // Handle category manifest
232
+ if (uri === "4da://categories") {
233
+ return {
234
+ contents: [
235
+ {
236
+ uri,
237
+ mimeType: "application/json",
238
+ text: JSON.stringify(getCategoryManifest(), null, 2),
239
+ },
240
+ ],
241
+ };
242
+ }
243
+ // Handle skill manifest
244
+ if (uri === "4da://skills") {
245
+ const skillsPath = join(homedir(), ".local", "share", "4da", "skills", "registry.json");
246
+ if (!existsSync(skillsPath)) {
247
+ throw new Error("Skill manifest not found. Run 4DA setup to create it.");
248
+ }
249
+ const skillsContent = readFileSync(skillsPath, "utf-8");
250
+ return {
251
+ contents: [
252
+ {
253
+ uri,
254
+ mimeType: "application/json",
255
+ text: skillsContent,
256
+ },
257
+ ],
258
+ };
248
259
  }
249
- const skillsContent = readFileSync(skillsPath, "utf-8");
260
+ // Parse tool name from URI: 4da://schema/{tool_name}
261
+ const match = uri.match(/^4da:\/\/schema\/(.+)$/);
262
+ if (!match) {
263
+ throw new Error(`Invalid resource URI: ${uri}`);
264
+ }
265
+ const toolName = match[1];
266
+ if (!hasToolSchema(toolName)) {
267
+ throw new Error(`Unknown tool: ${toolName}`);
268
+ }
269
+ const schemaFile = getSchemaFilename(toolName);
270
+ if (!schemaFile) {
271
+ throw new Error(`No schema file for tool: ${toolName}`);
272
+ }
273
+ // Read schema from file
274
+ const schemaPath = join(__dirname, "schemas", schemaFile);
275
+ const schemaContent = readFileSync(schemaPath, "utf-8");
250
276
  return {
251
277
  contents: [
252
278
  {
253
279
  uri,
254
280
  mimeType: "application/json",
255
- text: skillsContent,
256
- },
257
- ],
258
- };
259
- }
260
- // Parse tool name from URI: 4da://schema/{tool_name}
261
- const match = uri.match(/^4da:\/\/schema\/(.+)$/);
262
- if (!match) {
263
- throw new Error(`Invalid resource URI: ${uri}`);
264
- }
265
- const toolName = match[1];
266
- if (!hasToolSchema(toolName)) {
267
- throw new Error(`Unknown tool: ${toolName}`);
268
- }
269
- const schemaFile = getSchemaFilename(toolName);
270
- if (!schemaFile) {
271
- throw new Error(`No schema file for tool: ${toolName}`);
272
- }
273
- // Read schema from file
274
- const schemaPath = join(__dirname, "schemas", schemaFile);
275
- const schemaContent = readFileSync(schemaPath, "utf-8");
276
- return {
277
- contents: [
278
- {
279
- uri,
280
- mimeType: "application/json",
281
- text: schemaContent,
282
- },
283
- ],
284
- };
285
- });
286
- /**
287
- * Execute a tool
288
- */
289
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
290
- const { name, arguments: args } = request.params;
291
- try {
292
- const database = getDatabase();
293
- return await dispatchTool(name, database, args);
294
- }
295
- catch (error) {
296
- const errorMessage = error instanceof Error ? error.message : String(error);
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: JSON.stringify({ error: errorMessage }, null, 2),
281
+ text: schemaContent,
302
282
  },
303
283
  ],
304
- isError: true,
305
284
  };
306
- }
307
- });
285
+ });
286
+ // Execute a tool
287
+ server.setRequestHandler("tools/call", async (request) => {
288
+ const { name, arguments: args } = request.params;
289
+ try {
290
+ const database = getDatabase();
291
+ return await dispatchTool(name, database, args);
292
+ }
293
+ catch (error) {
294
+ const errorMessage = error instanceof Error ? error.message : String(error);
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: JSON.stringify({ error: errorMessage }, null, 2),
300
+ },
301
+ ],
302
+ isError: true,
303
+ };
304
+ }
305
+ });
306
+ return server;
307
+ }
308
308
  // =============================================================================
309
309
  // Server Lifecycle
310
310
  // =============================================================================
@@ -312,8 +312,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
312
312
  * Start the MCP server
313
313
  *
314
314
  * Supports three modes:
315
- * (default) stdio transport — classic MCP, works with all hosts
316
- * --http Streamable HTTP transport (spec 2025-03-26)
315
+ * (default) stdio transport — serves 2025-era and 2026-07-28 hosts
316
+ * --http Streamable HTTP transport — stateless, both eras
317
317
  * --setup Configure editors for 4DA MCP
318
318
  */
319
319
  async function main() {
@@ -368,7 +368,7 @@ async function main() {
368
368
  const portIndex = args.indexOf("--port");
369
369
  const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 4840;
370
370
  const host = args.includes("--host") ? args[args.indexOf("--host") + 1] : "127.0.0.1";
371
- await startHttpServer(server, { port, host });
371
+ await startHttpServer(buildServer, { port, host });
372
372
  return;
373
373
  }
374
374
  // -------------------------------------------------------------------------
@@ -392,9 +392,14 @@ async function main() {
392
392
  console.error(` Or run: npx @4da/mcp-server --doctor for diagnostics`);
393
393
  console.error(``);
394
394
  }
395
- // Default: stdio transport (existing behavior)
396
- const transport = new StdioServerTransport();
397
- await server.connect(transport);
395
+ // Default: stdio serving. `serveStdio` owns the era decision per connection —
396
+ // a 2025-era `initialize` opening is served exactly as before; a 2026-07-28
397
+ // `server/discover` opening gets the stateless modern protocol.
398
+ serveStdio(buildServer, {
399
+ onerror: (error) => {
400
+ console.error(`[4DA] stdio serving error: ${error.message}`);
401
+ },
402
+ });
398
403
  // Handle graceful shutdown
399
404
  process.on("SIGINT", () => {
400
405
  console.error("[4DA] Received SIGINT — shutting down gracefully");
@@ -409,7 +414,7 @@ async function main() {
409
414
  process.exit(0);
410
415
  });
411
416
  const toolCount = getSlimToolList().length;
412
- console.error(`4DA MCP Server v4.6.3 started — ${toolCount} tools, stdio transport`);
417
+ console.error(`4DA MCP Server v${SERVER_VERSION} started — ${toolCount} tools, stdio transport`);
413
418
  console.error(" Use --http for Streamable HTTP, --setup to configure editors, --doctor to check health");
414
419
  }
415
420
  main().catch((error) => {
package/dist/setup.js CHANGED
File without changes
@@ -4,6 +4,7 @@
4
4
  * Map-based dispatch for the 14 active tools.
5
5
  * Adding a new tool = add to this map + schema-registry + barrel export.
6
6
  */
7
+ import type { CallToolResult } from "@modelcontextprotocol/server";
7
8
  import type { FourDADatabase } from "./db.js";
8
9
  /**
9
10
  * Executor signature — all tool execute functions follow this shape.
@@ -14,12 +15,7 @@ export type ToolExecutor = (db: FourDADatabase, params: any) => unknown | Promis
14
15
  * Dispatch a tool call by name. Awaits uniformly (no-op for sync executors).
15
16
  * Returns MCP-formatted response with JSON-serialized result.
16
17
  */
17
- export declare function dispatchTool(name: string, db: FourDADatabase, args: Record<string, unknown> | undefined): Promise<{
18
- content: Array<{
19
- type: string;
20
- text: string;
21
- }>;
22
- }>;
18
+ export declare function dispatchTool(name: string, db: FourDADatabase, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
23
19
  /** Check if a tool exists in the dispatch map */
24
20
  export declare function hasDispatchTool(name: string): boolean;
25
21
  /** Get count of registered tool executors */
@@ -250,7 +250,11 @@ export function executeGetActionableSignals(db, params) {
250
250
  items = db.getRelevantContent(0.1, undefined, 200, 168); // Try 7 days
251
251
  }
252
252
  if (items.length === 0) {
253
- items = db.getRelevantContent(0.0, undefined, 200, 720); // Try 30 days, any score
253
+ // Deep 30-day/any-score fallback reaches the stale-epoch tail after a
254
+ // pipeline-version bump. This path trusts stored signal_type/signal_priority
255
+ // verbatim (confidence 0.90 below), so it MUST only see current-version
256
+ // rows — stale persisted signals are claims the live brain no longer makes.
257
+ items = db.getRelevantContent(0.0, undefined, 200, 720, true);
254
258
  }
255
259
  // Get user's detected tech for cross-referencing
256
260
  const context = db.getUserContext(true, false);
@@ -60,7 +60,10 @@ export function executeGetRelevantContent(db, params) {
60
60
  items = db.getRelevantContent(minScore, params.source_type, limit, 168);
61
61
  }
62
62
  if (items.length === 0) {
63
- items = db.getRelevantContent(0.0, params.source_type, limit, 720);
63
+ // Deep 30-day fallback at zero floor reaches the stale-epoch tail after a
64
+ // pipeline-version bump — require current-version scores so we never rank
65
+ // numbers the live scoring brain doesn't stand behind.
66
+ items = db.getRelevantContent(0.0, params.source_type, limit, 720, true);
64
67
  }
65
68
  return items;
66
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4da/mcp-server",
3
- "version": "4.6.6",
3
+ "version": "5.0.0",
4
4
  "mcpName": "io.github.4DA-Systems/4da-mcp-server",
5
5
  "description": "Dependency intelligence for AI coding agents. Live CVE scanning, dependency health, upgrade planning, ecosystem news, decision memory — zero config, privacy-first. 9 tools standalone, 14 total with desktop app.",
6
6
  "type": "module",
@@ -18,6 +18,17 @@
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
+ "scripts": {
22
+ "build": "tsc && pnpm run copy-schemas",
23
+ "copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
24
+ "dev": "tsc --watch",
25
+ "start": "node dist/index.js",
26
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
27
+ "setup": "node dist/setup.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "prepublishOnly": "pnpm run build"
31
+ },
21
32
  "keywords": [
22
33
  "mcp",
23
34
  "mcp-server",
@@ -69,8 +80,9 @@
69
80
  "url": "https://github.com/4DA-Systems/4DA/issues"
70
81
  },
71
82
  "dependencies": {
72
- "@modelcontextprotocol/sdk": "^1.29.0",
73
- "better-sqlite3": "^12.9.0"
83
+ "better-sqlite3": "^12.9.0",
84
+ "@modelcontextprotocol/node": "^2.0.0",
85
+ "@modelcontextprotocol/server": "^2.0.0"
74
86
  },
75
87
  "devDependencies": {
76
88
  "@types/better-sqlite3": "^7.6.11",
@@ -80,16 +92,18 @@
80
92
  "vitest": "^4.1.5"
81
93
  },
82
94
  "engines": {
83
- "node": ">=18.0.0"
95
+ "node": ">=20.0.0"
84
96
  },
85
- "scripts": {
86
- "build": "tsc && pnpm run copy-schemas",
87
- "copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
88
- "dev": "tsc --watch",
89
- "start": "node dist/index.js",
90
- "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
91
- "setup": "node dist/setup.js",
92
- "test": "vitest run",
93
- "test:watch": "vitest"
97
+ "pnpm": {
98
+ "onlyBuiltDependencies": [
99
+ "better-sqlite3",
100
+ "esbuild"
101
+ ],
102
+ "overrides": {
103
+ "fast-uri": ">=3.1.2",
104
+ "ip-address": "^10.1.1",
105
+ "vite": ">=7.3.5 <8",
106
+ "esbuild": ">=0.28.1"
107
+ }
94
108
  }
95
- }
109
+ }