subgraph-registry-mcp 0.9.7 → 0.9.9
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/data/openapi.json +1 -1
- package/openapi.yaml +1 -1
- package/package.json +3 -2
- package/src/index.js +88 -41
package/data/openapi.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Subgraph Registry",
|
|
5
5
|
"description": "Agent-friendly subgraph discovery on The Graph Network. 15,330 classified subgraphs with semantic search, reliability scoring, 30-day query volume and schema-evolution tracking. Discovery only: returns subgraph ids and starter queries, which you run with a Graph Studio API key (Authorization: Bearer) or over x402 ($0.01 USDC on Base, no key).",
|
|
6
|
-
"version": "0.9.
|
|
6
|
+
"version": "0.9.9",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT"
|
|
9
9
|
},
|
package/openapi.yaml
CHANGED
|
@@ -5,7 +5,7 @@ openapi: "3.1.0"
|
|
|
5
5
|
info:
|
|
6
6
|
title: "Subgraph Registry"
|
|
7
7
|
description: "Agent-friendly subgraph discovery on The Graph Network. 15,330 classified subgraphs with semantic search, reliability scoring, 30-day query volume and schema-evolution tracking. Discovery only: returns subgraph ids and starter queries, which you run with a Graph Studio API key (Authorization: Bearer) or over x402 ($0.01 USDC on Base, no key)."
|
|
8
|
-
version: "0.9.
|
|
8
|
+
version: "0.9.9"
|
|
9
9
|
license:
|
|
10
10
|
name: "MIT"
|
|
11
11
|
contact:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subgraph-registry-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"mcpName": "io.github.PaulieB14/subgraph-registry-mcp",
|
|
5
5
|
"description": "MCP server for agent-friendly subgraph discovery on The Graph Network. 15,330 classified subgraphs with reliability scoring, 30-day query volume, semantic search and protocol classification. Discovery only — returns subgraph ids and ready-to-run queries for you to run with a Graph Studio key or over x402.",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"start:http-only": "node src/index.js --http-only",
|
|
21
21
|
"test": "node --test test/",
|
|
22
22
|
"sync:server": "node scripts/sync-server-json.js",
|
|
23
|
-
"version": "node scripts/sync-server-json.js && node scripts/gen-openapi.js && git add server.json openapi.yaml data/openapi.json"
|
|
23
|
+
"version": "node scripts/sync-server-json.js && node scripts/gen-openapi.js && git add server.json openapi.yaml data/openapi.json",
|
|
24
|
+
"smoke": "node scripts/smoke-bin.mjs"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"mcp",
|
package/src/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import Database from "better-sqlite3";
|
|
|
24
24
|
import express from "express";
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
26
26
|
import { basename, dirname, join } from "path";
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "fs";
|
|
28
28
|
import { get as httpsGet } from "https";
|
|
29
29
|
import { createHash } from "crypto";
|
|
30
30
|
|
|
@@ -349,6 +349,40 @@ function queryTerms(query) {
|
|
|
349
349
|
.slice(0, 5);
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// Re-rank candidates on WORD boundaries, which SQL LIKE cannot express.
|
|
353
|
+
//
|
|
354
|
+
// The SQL score treats any substring of a display name as a name hit, so
|
|
355
|
+
// "scores" scored scoresquare-base at full name weight and "for" scored
|
|
356
|
+
// forsage-x2-prod, both beating genuine description matches. A term that
|
|
357
|
+
// appears as a whole word is a real signal; a term that is merely a prefix of
|
|
358
|
+
// a longer word is not.
|
|
359
|
+
//
|
|
360
|
+
// Shared by search_subgraphs and recommend_subgraph. It lived only in
|
|
361
|
+
// recommend, which is why `search_subgraphs("reputation scores for onchain
|
|
362
|
+
// agents")` still returned scoresquare-base at #1 while recommend did not —
|
|
363
|
+
// two tools disagreeing because a fix was applied to one of them.
|
|
364
|
+
function boundaryRerank(rows, words) {
|
|
365
|
+
if (!words.length) return rows;
|
|
366
|
+
const res = words.map(
|
|
367
|
+
(w) => new RegExp("(^|[^a-z0-9])" + w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "([^a-z0-9]|$)", "i"),
|
|
368
|
+
);
|
|
369
|
+
const score = (r) => {
|
|
370
|
+
const name = r.display_name || "";
|
|
371
|
+
const text = `${r.description || ""} ${r.auto_description || ""}`;
|
|
372
|
+
let out = 0;
|
|
373
|
+
res.forEach((re, i) => {
|
|
374
|
+
if (re.test(name)) out += 6;
|
|
375
|
+
else if (name.toLowerCase().includes(words[i])) out += 1;
|
|
376
|
+
if (re.test(text)) out += 2;
|
|
377
|
+
});
|
|
378
|
+
return out;
|
|
379
|
+
};
|
|
380
|
+
return rows
|
|
381
|
+
.map((r) => ({ r, s: score(r) }))
|
|
382
|
+
.sort((a, b) => b.s - a.s || (b.r.reliability_score || 0) - (a.r.reliability_score || 0))
|
|
383
|
+
.map((x) => x.r);
|
|
384
|
+
}
|
|
385
|
+
|
|
352
386
|
// ── Maturity / cold-start handling ─────────────────────────
|
|
353
387
|
// reliability_score is built from four CUMULATIVE inputs (curation signal,
|
|
354
388
|
// indexer stake, lifetime query fees, 30d volume — see _reliability_score in
|
|
@@ -542,7 +576,10 @@ function searchSubgraphs({
|
|
|
542
576
|
// Positional binding order: the SELECT-clause scoring expression is bound
|
|
543
577
|
// before the WHERE clause, so matchParams must lead. filterParams stays
|
|
544
578
|
// WHERE-only, which is what the emerging companion query needs.
|
|
545
|
-
const rows =
|
|
579
|
+
const rows = boundaryRerank(
|
|
580
|
+
getDb().prepare(sql).all(...matchParams, ...filterParams, fetchLimit),
|
|
581
|
+
query ? queryTerms(query) : [],
|
|
582
|
+
);
|
|
546
583
|
// Dedup by IPFS hash — keep highest reliability per deployment
|
|
547
584
|
const seenIpfs = new Set();
|
|
548
585
|
const results = [];
|
|
@@ -721,15 +758,27 @@ function recommendSubgraph({ goal, chain = "" }) {
|
|
|
721
758
|
if (canonical !== w || KNOWN_NETWORKS.has(canonical)) chainWords.push(canonical);
|
|
722
759
|
else words.push(w);
|
|
723
760
|
}
|
|
724
|
-
//
|
|
725
|
-
|
|
761
|
+
// A chain word NEVER becomes a scoring term. The first version of this fell
|
|
762
|
+
// back to `words.push(...chainWords)` whenever it could not use them as a
|
|
763
|
+
// filter — and because chainWords hold the CANONICAL form, "on ethereum"
|
|
764
|
+
// re-entered scoring as the token "mainnet" and scored +6 against any
|
|
765
|
+
// display name containing it. Passing chain:"ethereum" with a goal ending
|
|
766
|
+
// "on ethereum" therefore ranked Clearpool staking mainnet (vol 2) over Lido
|
|
767
|
+
// (4,692,414), Mainnet Voting V2 over Snapshot, seer-outcome-tokens-mainnet
|
|
768
|
+
// (vol 1) over ENS (34,835,842), and dropped EigenLayer out of the top 5
|
|
769
|
+
// entirely. It made the explicit chain argument actively worse than omitting
|
|
770
|
+
// it, which is the opposite of what an argument is for.
|
|
771
|
+
//
|
|
772
|
+
// Chain words are routing information. They filter, or they are discarded.
|
|
773
|
+
const explicitChain = chain ? normalizeNetwork(chain) : null;
|
|
774
|
+
if (!explicitChain && chainWords.length === 1) {
|
|
726
775
|
conditions.push("network = ?");
|
|
727
776
|
params.push(chainWords[0]);
|
|
728
|
-
inferredChain = chainWords[0];
|
|
729
|
-
} else if (chainWords.length) {
|
|
730
|
-
// Ambiguous or already-specified: keep them as weak text signal only.
|
|
731
|
-
words.push(...chainWords);
|
|
732
777
|
}
|
|
778
|
+
// Report the chain actually in effect, whether it came from the argument or
|
|
779
|
+
// the prose — a caller passing chain used to get inferred_chain: null, which
|
|
780
|
+
// read as "your chain was ignored".
|
|
781
|
+
inferredChain = explicitChain || (chainWords.length === 1 ? chainWords[0] : null);
|
|
733
782
|
|
|
734
783
|
if (words.length) {
|
|
735
784
|
const textConds = words.map(() => "(display_name LIKE ? OR description LIKE ? OR auto_description LIKE ?)");
|
|
@@ -767,37 +816,8 @@ function recommendSubgraph({ goal, chain = "" }) {
|
|
|
767
816
|
// SELECT-clause params bind before WHERE-clause params.
|
|
768
817
|
let rows = getDb().prepare(sql).all(...scoreParams, ...params);
|
|
769
818
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
// The SQL score treats any substring of the display name as a name hit, so
|
|
773
|
-
// "scores" scored scoresquare-base at 4 and "for" scored forsage-x2-prod at
|
|
774
|
-
// 4, both beating agent0's two genuine description matches at 1 apiece. A
|
|
775
|
-
// term that appears as a whole word in the name is a real signal; a term that
|
|
776
|
-
// merely happens to be a prefix of a longer word is not, and was outranking
|
|
777
|
-
// it 4 to 1.
|
|
778
|
-
//
|
|
779
|
-
// Done here rather than in SQL because expressing "\bterm\b" in LIKE needs
|
|
780
|
-
// half a dozen OR-ed patterns per word for space, hyphen and underscore
|
|
781
|
-
// delimiters. The SQL score stays as the recall net (LIMIT 60); this decides
|
|
782
|
-
// the order of what it caught.
|
|
783
|
-
if (words.length) {
|
|
784
|
-
const bounded = words.map((w) => new RegExp("(^|[^a-z0-9])" + w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "([^a-z0-9]|$)", "i"));
|
|
785
|
-
const rescore = (r) => {
|
|
786
|
-
const name = r.display_name || "";
|
|
787
|
-
const text = `${r.description || ""} ${r.auto_description || ""}`;
|
|
788
|
-
let score = 0;
|
|
789
|
-
bounded.forEach((re, i) => {
|
|
790
|
-
if (re.test(name)) score += 6; // whole word in the name
|
|
791
|
-
else if (name.toLowerCase().includes(words[i])) score += 1; // incidental substring
|
|
792
|
-
if (re.test(text)) score += 2; // whole word in the description
|
|
793
|
-
});
|
|
794
|
-
return score;
|
|
795
|
-
};
|
|
796
|
-
rows = rows
|
|
797
|
-
.map((r) => ({ r, s: rescore(r) }))
|
|
798
|
-
.sort((a, b) => b.s - a.s || (b.r.reliability_score || 0) - (a.r.reliability_score || 0))
|
|
799
|
-
.map((x) => x.r);
|
|
800
|
-
}
|
|
819
|
+
rows = boundaryRerank(rows, words);
|
|
820
|
+
|
|
801
821
|
// De-dup first so we batch the stability lookup over the trimmed set.
|
|
802
822
|
const seenIpfs = new Set();
|
|
803
823
|
const keep = [];
|
|
@@ -1856,10 +1876,37 @@ export { TOOLS };
|
|
|
1856
1876
|
// the MCP server or open the SQLite file. Use pathToFileURL so the
|
|
1857
1877
|
// comparison works on Windows where process.argv[1] uses backslashes
|
|
1858
1878
|
// while import.meta.url is forward-slashed.
|
|
1879
|
+
// Whether this file was RUN, as opposed to imported (scripts/gen-openapi.js
|
|
1880
|
+
// imports it for TOOLS[] and must not spawn a server).
|
|
1881
|
+
//
|
|
1882
|
+
// This has to compare real paths. npm installs a bin as a symlink —
|
|
1883
|
+
// node_modules/.bin/subgraph-registry-mcp -> ../subgraph-registry-mcp/src/index.js
|
|
1884
|
+
// — and Node sets import.meta.url to the RESOLVED target while argv[1] keeps
|
|
1885
|
+
// the symlink. So the old comparison was:
|
|
1886
|
+
//
|
|
1887
|
+
// pathToFileURL(argv[1]) = file:///…/node_modules/.bin/subgraph-registry-mcp
|
|
1888
|
+
// import.meta.url = file:///…/subgraph-registry-mcp/src/index.js
|
|
1889
|
+
// basename(argv[1]) = "subgraph-registry-mcp" (not "index.js")
|
|
1890
|
+
//
|
|
1891
|
+
// Both branches false, so main() never ran: the process exited 0, instantly,
|
|
1892
|
+
// silently, and every MCP host reported "Connection closed" with 0 tools. It
|
|
1893
|
+
// worked in local testing only because `node src/index.js` satisfies the
|
|
1894
|
+
// basename fallback — which is exactly why no test caught it. The package has
|
|
1895
|
+
// never once started over npx.
|
|
1896
|
+
//
|
|
1897
|
+
// realpathSync collapses the symlink on both sides, so npx, `npm i -g`, a
|
|
1898
|
+
// direct path and a symlinked directory all agree. Note the URL comparison was
|
|
1899
|
+
// already unreliable on its own: on macOS a /var path resolves to /private/var,
|
|
1900
|
+
// so even a direct run failed that branch and survived on the basename check.
|
|
1859
1901
|
const _isMain =
|
|
1860
1902
|
process.argv[1] !== undefined &&
|
|
1861
|
-
(
|
|
1862
|
-
|
|
1903
|
+
(() => {
|
|
1904
|
+
try {
|
|
1905
|
+
return realpathSync(process.argv[1]) === realpathSync(__filename);
|
|
1906
|
+
} catch {
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
})();
|
|
1863
1910
|
|
|
1864
1911
|
if (_isMain) {
|
|
1865
1912
|
main().catch((err) => {
|