@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 +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.js +72 -37
- package/dist/http-transport.d.ts +12 -8
- package/dist/http-transport.js +23 -57
- package/dist/index.d.ts +7 -24
- package/dist/index.js +133 -128
- package/dist/setup.js +0 -0
- package/dist/tool-dispatch.d.ts +2 -6
- package/dist/tools/get-actionable-signals.js +5 -1
- package/dist/tools/get-relevant-content.js +4 -1
- package/package.json +28 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@4da/mcp-server)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
// Anti-penalty from exclusions
|
|
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.
|
|
1093
|
-
save: 0
|
|
1094
|
-
dismiss: -0.
|
|
1095
|
-
mark_irrelevant: -0
|
|
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
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
/**
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 4DA MCP Server — Streamable HTTP Transport
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
14
|
+
* - Stateless (no session tracking)
|
|
11
15
|
*/
|
|
12
|
-
import { Server } from "@modelcontextprotocol/
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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(
|
|
28
|
+
export declare function startHttpServer(factory: () => Server, options?: HttpServerOptions): Promise<void>;
|
|
25
29
|
//# sourceMappingURL=http-transport.d.ts.map
|
package/dist/http-transport.js
CHANGED
|
@@ -2,52 +2,44 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 4DA MCP Server — Streamable HTTP Transport
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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(
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
4
|
+
* Build a Server instance with every handler registered.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
30
|
-
import {
|
|
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
|
-
//
|
|
178
|
+
// Server Factory
|
|
187
179
|
// =============================================================================
|
|
188
180
|
/**
|
|
189
|
-
*
|
|
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
|
-
*
|
|
204
|
-
*
|
|
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
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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:
|
|
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 —
|
|
316
|
-
* --http Streamable HTTP transport
|
|
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(
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
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
|
package/dist/tool-dispatch.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
73
|
-
"
|
|
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": ">=
|
|
95
|
+
"node": ">=20.0.0"
|
|
84
96
|
},
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|