subgraph-registry-mcp 0.8.30 → 0.9.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
@@ -159,6 +159,43 @@ All values are log-scaled and capped at 1.0. A 0.5 penalty is applied if the sub
159
159
 
160
160
  **Score tiers:** High (0.7+) = strong signal, real usage | Medium (0.3-0.7) = functional, some activity | Low (<0.3) = minimal signal or test deployment
161
161
 
162
+ ### The score measures traction, so it measures age
163
+
164
+ All four inputs are cumulative — fees and curation accrue, volume needs 30 days
165
+ to exist at all. A subgraph deployed last month therefore scores near zero no
166
+ matter how good it is. Measured on the current corpus (served, non-denied):
167
+
168
+ | Age | Count | Avg reliability |
169
+ |-----|-------|-----------------|
170
+ | < 30 days | 64 | 0.107 |
171
+ | 30–90 days | 227 | 0.143 |
172
+ | 90–365 days | 1,100 | 0.225 |
173
+ | > 1 year | 4,034 | 0.313 |
174
+
175
+ The newest subgraph anywhere in the registry's top 25 is **280 days old** — yet
176
+ 59 of those 64 sub-30-day subgraphs are already serving real query volume.
177
+
178
+ Rather than reweight the score and trade a measurable signal for a guess,
179
+ `search_subgraphs` returns young matches in a **separate `emerging` list**
180
+ alongside an `emerging_caveat` explaining that a low score at that age is
181
+ expected rather than damning. Every result also carries `age_days` and
182
+ `maturity` (`new` < 30d, `emerging` < 90d, `established`). This matters most
183
+ for new chains and new protocols, where no mature deployment *can* exist —
184
+ searching "perpetual futures" surfaces years-old Ethereum and BSC deployments
185
+ in the main list and the 40-day-old Monad perps subgraph under `emerging`.
186
+
187
+ `semantic_search_subgraphs` ranks by cosine similarity rather than reliability,
188
+ so it is already age-neutral — it carries the `maturity` labels but no
189
+ `emerging` list, because a three-week-old subgraph can top it on merit.
190
+
191
+ ### Denied deployments
192
+
193
+ Curation-denied deployments (`deniedAt > 0` — denied indexing rewards, usually
194
+ spam, duplicates or deprecations) are **excluded by default** from
195
+ `search_subgraphs`, `semantic_search_subgraphs` and `recommend_subgraph`. Pass
196
+ `include_denied: true` to the two search tools to see them; every result then
197
+ carries `denied: true|false` so the choice stays visible.
198
+
162
199
  ---
163
200
 
164
201
  ## MCP Server
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. 14,700+ classified subgraphs with semantic search, reliability scoring, schema-evolution tracking, and x402 query URLs ($0.01 USDC on Base, no API key).",
6
- "version": "0.8.30",
6
+ "version": "0.8.31",
7
7
  "license": {
8
8
  "name": "MIT"
9
9
  },
package/data/registry.db CHANGED
Binary file
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. 14,700+ classified subgraphs with semantic search, reliability scoring, schema-evolution tracking, and x402 query URLs ($0.01 USDC on Base, no API key)."
8
- version: "0.8.30"
8
+ version: "0.8.31"
9
9
  license:
10
10
  name: "MIT"
11
11
  contact:
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "subgraph-registry-mcp",
3
- "version": "0.8.30",
3
+ "version": "0.9.0",
4
4
  "mcpName": "io.github.PaulieB14/subgraph-registry-mcp",
5
- "description": "MCP server for agent-friendly subgraph discovery on The Graph Network. 15,324 classified subgraphs with x402 query URLs ($0.01 USDC on Base, no API key required), reliability scoring, and protocol classification.",
5
+ "description": "MCP server for agent-friendly subgraph discovery on The Graph Network. 15,330 classified subgraphs with x402 query URLs ($0.01 USDC on Base, no API key required), reliability scoring, and protocol classification.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "subgraph-registry-mcp": "src/index.js"
@@ -17,7 +17,8 @@
17
17
  "scripts": {
18
18
  "start": "node src/index.js",
19
19
  "start:http": "node src/index.js --http",
20
- "start:http-only": "node src/index.js --http-only"
20
+ "start:http-only": "node src/index.js --http-only",
21
+ "test": "node --test test/"
21
22
  },
22
23
  "keywords": [
23
24
  "mcp",
package/src/index.js CHANGED
@@ -30,6 +30,21 @@ import { createHash } from "crypto";
30
30
 
31
31
  const __filename = fileURLToPath(import.meta.url);
32
32
  const __dirname = dirname(__filename);
33
+
34
+ // Read from package.json rather than a literal. The hardcoded string had
35
+ // drifted to "0.8.20" while the package shipped 0.8.31 — eleven releases of
36
+ // every MCP client being told the wrong server version, which is the kind of
37
+ // thing that only surfaces when someone is debugging something else.
38
+ const PKG_VERSION = (() => {
39
+ try {
40
+ return JSON.parse(
41
+ readFileSync(join(__dirname, "..", "package.json"), "utf8"),
42
+ ).version;
43
+ } catch {
44
+ return "0.0.0";
45
+ }
46
+ })();
47
+
33
48
  const DATA_DIR = join(__dirname, "..", "data");
34
49
  const DB_PATH = join(DATA_DIR, "registry.db");
35
50
  const OPENAPI_JSON_PATH = join(DATA_DIR, "openapi.json");
@@ -50,7 +65,7 @@ const GITHUB_DB_URL =
50
65
  // 3. Paste the new hash here and bump package.json version
51
66
  // 4. Update SKILL.md "Verifying the registry" section
52
67
  const EXPECTED_DB_SHA256 =
53
- "e262ff75220727f525f8bc974fb022190c5141d251ed4007ec237a1cb50bd62b";
68
+ "ec7bb54f72379c0d4921b0c34c1c666c46e44c82f334efda440aeb2db09fa2d0";
54
69
  // Skip-verification escape hatch (set to "1" only if you're rebuilding the DB
55
70
  // locally and know what you're doing — never set in agent-runtime defaults).
56
71
  const SKIP_VERIFY = process.env.SUBGRAPH_REGISTRY_SKIP_VERIFY === "1";
@@ -162,6 +177,84 @@ function getDb() {
162
177
  return db;
163
178
  }
164
179
 
180
+ // ── Maturity / cold-start handling ─────────────────────────
181
+ // reliability_score is built from four CUMULATIVE inputs (curation signal,
182
+ // indexer stake, lifetime query fees, 30d volume — see _reliability_score in
183
+ // python/classifier.py), so it measures accrued traction and therefore age.
184
+ // Measured on the 2026-08-25 corpus, served and non-denied:
185
+ //
186
+ // age n avg reliability
187
+ // <30d 64 0.107
188
+ // 30-90d 227 0.143
189
+ // 90-365d 1100 0.225
190
+ // >1y 4034 0.313
191
+ //
192
+ // The newest entry in the global top 25 is 280 days old. A good subgraph
193
+ // deployed last month cannot rank, no matter how good it is — 59 of those 64
194
+ // sub-30-day subgraphs are already serving real query volume.
195
+ //
196
+ // Rather than reweight the score (which would silently change every existing
197
+ // caller's results and trade a measurable signal for a guess), surface the
198
+ // young matches SEPARATELY and label them honestly, so the agent makes the
199
+ // call. A label alone would not be enough: at 0.107 average, young subgraphs
200
+ // are not in the ranked page to be labelled, so this needs its own lookup.
201
+ const EMERGING_MAX_AGE_DAYS = 90;
202
+ const NEW_MAX_AGE_DAYS = 30;
203
+ const EMERGING_LIMIT = 3;
204
+
205
+ // Shared so the main search and its emerging companion query select exactly
206
+ // the same columns — they feed the same row mapper.
207
+ const SEARCH_COLS = `id, display_name, description, auto_description, domain, protocol_type, network,
208
+ reliability_score, ipfs_hash, entity_count, canonical_entities,
209
+ powered_by_substreams, active_allocation_count, example_query, denied_at, created_at`;
210
+
211
+ function ageDays(created_at) {
212
+ if (!created_at) return null;
213
+ return Math.max(0, Math.floor(Date.now() / 1000 - created_at) / 86400) | 0;
214
+ }
215
+
216
+ function maturityOf(created_at) {
217
+ const d = ageDays(created_at);
218
+ if (d === null) return "unknown";
219
+ if (d < NEW_MAX_AGE_DAYS) return "new";
220
+ if (d < EMERGING_MAX_AGE_DAYS) return "emerging";
221
+ return "established";
222
+ }
223
+
224
+ const EMERGING_CAVEAT =
225
+ "Recent deployments that matched your query but ranked below the main list " +
226
+ "because reliability_score is cumulative (curation signal, indexer stake, " +
227
+ "lifetime query fees, 30d volume) and so grows with age — the newest subgraph " +
228
+ "in the whole registry's top 25 is 280 days old. A low score here is expected " +
229
+ "at this age and is NOT evidence of a problem; these are unproven, not bad. " +
230
+ "Prefer `subgraphs` for production work. Consider these when the protocol is " +
231
+ "itself new (no mature deployment can exist), when you want the newest schema, " +
232
+ "or when the ranked list missed what you asked for.";
233
+
234
+ // Young matches for the same filter, fetched separately because they cannot
235
+ // compete on the main ORDER BY. Returns [] on any failure — this is an
236
+ // enrichment, and it must never take down the search that already succeeded.
237
+ function findEmerging(where, params, excludeIds, selectCols) {
238
+ try {
239
+ const cutoff = Math.floor(Date.now() / 1000) - EMERGING_MAX_AGE_DAYS * 86400;
240
+ const notIn = excludeIds.length
241
+ ? ` AND id NOT IN (${excludeIds.map(() => "?").join(",")})`
242
+ : "";
243
+ const sql = `
244
+ SELECT ${selectCols}
245
+ FROM subgraphs
246
+ ${where ? `${where} AND` : "WHERE"} created_at > ?${notIn}
247
+ ORDER BY reliability_score DESC
248
+ LIMIT ?
249
+ `;
250
+ return getDb()
251
+ .prepare(sql)
252
+ .all(...params, cutoff, ...excludeIds, EMERGING_LIMIT);
253
+ } catch {
254
+ return [];
255
+ }
256
+ }
257
+
165
258
  // ── Tool Implementations ───────────────────────────────────
166
259
 
167
260
  function searchSubgraphs({
@@ -172,6 +265,7 @@ function searchSubgraphs({
172
265
  entity = "",
173
266
  min_reliability = 0,
174
267
  include_unserved = false,
268
+ include_denied = false,
175
269
  limit = 20,
176
270
  } = {}) {
177
271
  const conditions = [];
@@ -183,6 +277,20 @@ function searchSubgraphs({
183
277
  conditions.push("active_allocation_count > 0");
184
278
  }
185
279
 
280
+ // Default: hide curation-denied deployments (deniedAt > 0 on the network
281
+ // subgraph — denied indexing rewards, typically spam, duplicates or
282
+ // deprecations). classifier.py has persisted this since the denied_at
283
+ // column landed, explicitly "so agents can filter denied subgraphs", but
284
+ // nothing in this file ever read it, so the filter the crawler was feeding
285
+ // did not exist. The 0.5 reliability penalty already keeps denied entries
286
+ // out of the top of a ranked page — measured: 0 of the global top 20, and
287
+ // none in any domain's top 10 — so this changes few results today. It
288
+ // matters for the narrow queries where a denied fork IS the best textual
289
+ // match, and it makes the signal visible either way via `denied` below.
290
+ if (!include_denied) {
291
+ conditions.push("denied_at = 0");
292
+ }
293
+
186
294
  if (domain) {
187
295
  conditions.push("domain = ?");
188
296
  params.push(domain);
@@ -219,14 +327,15 @@ function searchSubgraphs({
219
327
  // Over-fetch to allow dedup by IPFS hash (same deployment, different subgraph IDs)
220
328
  const fetchLimit = limit * 3;
221
329
  const sql = `
222
- SELECT id, display_name, description, auto_description, domain, protocol_type, network,
223
- reliability_score, ipfs_hash, entity_count, canonical_entities,
224
- powered_by_substreams, active_allocation_count, example_query
330
+ SELECT ${SEARCH_COLS}
225
331
  FROM subgraphs
226
332
  ${where}
227
333
  ORDER BY reliability_score DESC
228
334
  LIMIT ?
229
335
  `;
336
+ // Snapshot the filter params BEFORE the LIMIT is appended — the emerging
337
+ // companion query reuses the same WHERE and must not inherit this LIMIT.
338
+ const filterParams = [...params];
230
339
  params.push(fetchLimit);
231
340
 
232
341
  const rows = getDb().prepare(sql).all(...params);
@@ -249,17 +358,54 @@ function searchSubgraphs({
249
358
  canonical_entities: JSON.parse(r.canonical_entities),
250
359
  powered_by_substreams: Boolean(r.powered_by_substreams),
251
360
  active_allocation_count: r.active_allocation_count || 0,
361
+ // Curation-denied on the network subgraph. Only ever true when the
362
+ // caller passed include_denied — surfaced so that choice stays visible
363
+ // in the result rather than being silently carried.
364
+ denied: Boolean(r.denied_at),
252
365
  // Ready-to-run GraphQL generated from this subgraph's actual schema — so an
253
366
  // agent can POST it to query_url_x402 immediately, no get_subgraph_detail round-trip.
254
367
  example_query: r.example_query || null,
368
+ age_days: ageDays(r.created_at),
369
+ maturity: maturityOf(r.created_at),
255
370
  ...buildQueryEndpoints(r.id),
256
371
  });
257
372
  if (results.length >= limit) break;
258
373
  }
259
374
 
375
+ // Young matches that could not compete on the cumulative score. Same filter,
376
+ // separate lookup, clearly labelled — never blended into `subgraphs`, so a
377
+ // caller that ignores this field sees exactly what it saw before.
378
+ const emerging = findEmerging(
379
+ where,
380
+ filterParams,
381
+ results.map((x) => x.id),
382
+ SEARCH_COLS,
383
+ ).map((r) => ({
384
+ id: r.id,
385
+ display_name: r.display_name,
386
+ description: (r.description || r.auto_description || "").slice(0, 300),
387
+ domain: r.domain,
388
+ protocol_type: r.protocol_type,
389
+ network: r.network,
390
+ reliability_score: r.reliability_score,
391
+ ipfs_hash: r.ipfs_hash,
392
+ entity_count: r.entity_count,
393
+ canonical_entities: JSON.parse(r.canonical_entities),
394
+ powered_by_substreams: Boolean(r.powered_by_substreams),
395
+ active_allocation_count: r.active_allocation_count || 0,
396
+ denied: Boolean(r.denied_at),
397
+ example_query: r.example_query || null,
398
+ age_days: ageDays(r.created_at),
399
+ maturity: maturityOf(r.created_at),
400
+ ...buildQueryEndpoints(r.id),
401
+ }));
402
+
260
403
  return {
261
404
  total: results.length,
262
405
  subgraphs: results,
406
+ ...(emerging.length
407
+ ? { emerging, emerging_caveat: EMERGING_CAVEAT }
408
+ : {}),
263
409
  query_instructions: "Two ways to query: (a) RECOMMENDED — POST GraphQL to query_url_x402 and pay $0.01 USDC on Base per query via x402 (no API key required; gateway returns HTTP 402 with a payment manifest, use an x402 client like @graphprotocol/client-x402 to sign and retry). (b) LEGACY — replace [api-key] in query_url with a Graph API key from https://thegraph.com/studio/apikeys/. Each result includes a ready-to-run `example_query` generated from that subgraph's real schema — POST it to query_url_x402 as-is, or adapt the entity/fields. Use get_subgraph_detail for the full schema.",
264
410
  };
265
411
  }
@@ -295,7 +441,10 @@ function recommendSubgraph({ goal, chain = "" }) {
295
441
  .filter(([, kws]) => kws.some((k) => goalLower.includes(k)))
296
442
  .map(([t]) => t);
297
443
 
298
- const conditions = ["active_allocation_count > 0"];
444
+ // recommend_subgraph exposes no include_* escape hatches — it answers "which
445
+ // one should I use", so a curation-denied deployment is never the right
446
+ // answer and the filter is unconditional here.
447
+ const conditions = ["active_allocation_count > 0", "denied_at = 0"];
299
448
  const params = [];
300
449
 
301
450
  if (chain) {
@@ -589,6 +738,7 @@ async function semanticSearchSubgraphs({
589
738
  limit = 10,
590
739
  min_score = 0.3,
591
740
  include_unserved = false,
741
+ include_denied = false,
592
742
  domain = "",
593
743
  network = "",
594
744
  protocol_type = "",
@@ -615,6 +765,9 @@ async function semanticSearchSubgraphs({
615
765
  if (!include_unserved) {
616
766
  conditions.push("active_allocation_count > 0");
617
767
  }
768
+ if (!include_denied) {
769
+ conditions.push("denied_at = 0");
770
+ }
618
771
  if (domain) {
619
772
  conditions.push("domain = ?");
620
773
  params.push(domain);
@@ -637,7 +790,7 @@ async function semanticSearchSubgraphs({
637
790
  `SELECT id, display_name, description, auto_description, domain,
638
791
  protocol_type, network, reliability_score, ipfs_hash,
639
792
  entity_count, canonical_entities, powered_by_substreams,
640
- active_allocation_count, example_query, embedding
793
+ active_allocation_count, example_query, denied_at, created_at, embedding
641
794
  FROM subgraphs
642
795
  ${where}`,
643
796
  )
@@ -672,7 +825,15 @@ async function semanticSearchSubgraphs({
672
825
  canonical_entities: JSON.parse(r.canonical_entities),
673
826
  powered_by_substreams: Boolean(r.powered_by_substreams),
674
827
  active_allocation_count: r.active_allocation_count || 0,
828
+ denied: Boolean(r.denied_at),
675
829
  example_query: r.example_query || null,
830
+ // No `emerging` companion list here: this tool ranks by cosine score,
831
+ // not reliability_score, so a three-week-old subgraph can and does take
832
+ // the top slot on merit. The cold-start bias is a property of the ranked
833
+ // search, not of semantic search — what this tool needs is the label, so
834
+ // a caller knows a strong match is also unproven.
835
+ age_days: ageDays(r.created_at),
836
+ maturity: maturityOf(r.created_at),
676
837
  semantic_score: Number(score.toFixed(4)),
677
838
  ...buildQueryEndpoints(r.id),
678
839
  });
@@ -828,7 +989,7 @@ const TOOLS = [
828
989
  {
829
990
  name: "search_subgraphs",
830
991
  description:
831
- "Search and filter the classified subgraph registry (15,500+ subgraphs). Filter by domain (defi, nfts, dao, gaming, identity, infrastructure, social, analytics), network (mainnet, arbitrum-one, base, matic, bsc, optimism, avalanche), protocol_type (dex, lending, bridge, staking, options, perpetuals, nft-marketplace, yield-aggregator, governance, name-service), canonical entity type (liquidity_pool, trade, token, position, vault, loan, collateral, liquidation, nft_collection, nft_item, nft_sale, proposal, delegate, domain_name, account, transaction, daily_snapshot, hourly_snapshot), or free-text keyword. Returns subgraphs ranked by reliability score. Each result includes query_url_x402 (POST GraphQL and pay $0.01 USDC on Base per query — no API key needed) and a legacy query_url (Studio API key required).",
992
+ "Search and filter the classified subgraph registry (15,500+ subgraphs). Filter by domain (defi, nfts, dao, gaming, identity, infrastructure, social, analytics), network (mainnet, arbitrum-one, base, matic, bsc, optimism, avalanche), protocol_type (dex, lending, bridge, staking, options, perpetuals, nft-marketplace, yield-aggregator, governance, name-service), canonical entity type (liquidity_pool, trade, token, position, vault, loan, collateral, liquidation, nft_collection, nft_item, nft_sale, proposal, delegate, domain_name, account, transaction, daily_snapshot, hourly_snapshot), or free-text keyword. Returns subgraphs ranked by reliability score. Each result includes query_url_x402 (POST GraphQL and pay $0.01 USDC on Base per query — no API key needed) and a legacy query_url (Studio API key required), plus age_days and maturity (new | emerging | established). Because reliability_score is cumulative it structurally favours older deployments, so a separate `emerging` list carries recent matches that ranked below the main cut for age rather than quality — read `emerging_caveat` before offering one to a user.",
832
993
  inputSchema: {
833
994
  type: "object",
834
995
  additionalProperties: false,
@@ -838,8 +999,18 @@ const TOOLS = [
838
999
  network: { type: "string", description: "Filter by chain: mainnet, arbitrum-one, base, matic, bsc, optimism, avalanche, etc." },
839
1000
  protocol_type: { type: "string", description: "Filter by protocol type: dex, lending, bridge, staking, options, perpetuals, etc." },
840
1001
  entity: { type: "string", description: "Filter by canonical entity: liquidity_pool, trade, token, position, vault, loan, etc." },
841
- min_reliability: { type: "number", description: "Minimum reliability score (0-1). Higher = more query fees, volume, curation signal, and indexer allocation." },
1002
+ min_reliability: { type: "number", description: "Minimum reliability score (0-1). Higher = more query fees, volume, curation signal, and indexer allocation. NOTE: all four inputs are cumulative, so this score rises with age — setting a floor here filters out good recent subgraphs along with bad ones." },
842
1003
  limit: { type: "integer", description: "Max results to return (default: 20)", default: 20 },
1004
+ include_unserved: {
1005
+ type: "boolean",
1006
+ description: "Include subgraphs with 0 active indexer allocations (returns 'no allocations' on query). Default false.",
1007
+ default: false,
1008
+ },
1009
+ include_denied: {
1010
+ type: "boolean",
1011
+ description: "Include curation-denied deployments (deniedAt > 0 — denied indexing rewards, typically spam, duplicates or deprecations). Default false. When true, each result carries denied: true so the choice stays visible.",
1012
+ default: false,
1013
+ },
843
1014
  },
844
1015
  },
845
1016
  },
@@ -909,6 +1080,11 @@ const TOOLS = [
909
1080
  description: "Include subgraphs with 0 active indexer allocations (returns 'no allocations' on query). Default false.",
910
1081
  default: false,
911
1082
  },
1083
+ include_denied: {
1084
+ type: "boolean",
1085
+ description: "Include curation-denied deployments (deniedAt > 0 — denied indexing rewards, typically spam, duplicates or deprecations). Default false. When true, each result carries denied: true so the choice stays visible.",
1086
+ default: false,
1087
+ },
912
1088
  domain: {
913
1089
  type: "string",
914
1090
  description: "Pre-filter by domain (defi, nfts, dao, gaming, identity, infrastructure, social, analytics)",
@@ -963,7 +1139,7 @@ const HANDLERS = {
963
1139
 
964
1140
  function createServer() {
965
1141
  const server = new Server(
966
- { name: "subgraph-registry", version: "0.8.20" },
1142
+ { name: "subgraph-registry", version: PKG_VERSION },
967
1143
  { capabilities: { tools: {} } }
968
1144
  );
969
1145