mpp32-mcp-server 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { signX402Payment } from "./x402-signers.js";
6
- const SERVER_VERSION = "1.4.0";
6
+ const SERVER_VERSION = "1.4.1";
7
7
  // ── Env loading: trim and sanitize aggressively ─────────────────────────────
8
8
  // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
9
9
  // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
@@ -575,7 +575,10 @@ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discove
575
575
  }
576
576
  });
577
577
  // ── Tool 7: get_pivx_dao_intelligence ─────────────────────────────────────
578
- server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No/Abstain percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days. Data sourced from pivx.org/proposals and the PIVX blockchain. Free — no payment required.", {
578
+ // Self-contained: scrapes pivx.org/proposals + Chainz CryptoID directly.
579
+ // Does NOT depend on any backend endpoint — works for every npm user out of the box.
580
+ import { fetchPivxGovernance } from "./pivx-provider.js";
581
+ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No counts, net yes percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days (43,200 blocks per superblock cycle, 432,000 PIV max monthly budget). Data scraped live from pivx.org/proposals and the PIVX blockchain via Chainz CryptoID. Cached for 5 minutes. Free — no payment or API key required.", {
579
582
  filter: z
580
583
  .enum(["all", "passing", "failing"])
581
584
  .default("all")
@@ -588,52 +591,31 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
588
591
  .describe("Include network stats and deflation metrics (default: true)."),
589
592
  }, async ({ filter, includeStats }) => {
590
593
  try {
591
- const statusFilter = filter === "passing" || filter === "failing" ? `?status=${filter}` : "";
592
- const endpoint = includeStats !== false ? "/api/governance" : `/api/governance/proposals${statusFilter}`;
593
- const res = await fetchWithTimeout(`${API_URL}${endpoint}`, {
594
- headers: buildHeaders(),
595
- });
596
- if (!res.ok) {
597
- const err = await res.json().catch(() => null);
598
- return {
599
- content: [{
600
- type: "text",
601
- text: `Error fetching PIVX governance data: ${err?.error?.message ?? `HTTP ${res.status}`}`,
602
- }],
603
- };
604
- }
605
- const json = await res.json();
606
- const data = json.data;
607
- // Build readable output
594
+ const gov = await fetchPivxGovernance();
608
595
  const lines = [];
609
596
  lines.push("# PIVX DAO Governance Intelligence");
610
597
  lines.push("");
611
- // Network stats
612
- const network = data.network;
613
- if (network) {
598
+ if (includeStats !== false) {
599
+ const n = gov.network;
614
600
  lines.push("## Network Overview");
615
- lines.push(`- **Masternodes Online:** ${network.masternodeCount}`);
616
- lines.push(`- **Passing Threshold:** ${network.passingThreshold} votes (10% of masternodes)`);
617
- lines.push(`- **Monthly Budget:** ${Number(network.monthlyBudgetPiv).toLocaleString()} PIV (~$${Number(network.monthlyBudgetUsd).toLocaleString()})`);
618
- lines.push(`- **Budget Allocated:** ${Number(network.budgetAllocatedPiv).toLocaleString()} PIV (${network.budgetAllocatedPercent}%)`);
619
- if (network.blockHeight)
620
- lines.push(`- **Block Height:** ${Number(network.blockHeight).toLocaleString()}`);
621
- if (network.totalSupply)
622
- lines.push(`- **Total Supply:** ${Math.round(Number(network.totalSupply)).toLocaleString()} PIV`);
601
+ lines.push(`- **Masternodes Online:** ${n.masternodeCount.toLocaleString()}`);
602
+ lines.push(`- **Passing Threshold:** ${n.passingThreshold} votes (10% of masternodes)`);
603
+ lines.push(`- **Monthly Budget:** ${n.monthlyBudgetPiv.toLocaleString()} PIV (~$${n.monthlyBudgetUsd.toLocaleString()})`);
604
+ lines.push(`- **Budget Allocated:** ${n.budgetAllocatedPiv.toLocaleString()} PIV (${n.budgetAllocatedPercent}%)`);
605
+ if (n.blockHeight)
606
+ lines.push(`- **Block Height:** ${n.blockHeight.toLocaleString()}`);
607
+ if (n.totalSupply)
608
+ lines.push(`- **Total Supply:** ${Math.round(n.totalSupply).toLocaleString()} PIV`);
623
609
  lines.push("");
624
- }
625
- // Deflation stats
626
- const deflation = data.deflation;
627
- if (deflation) {
610
+ const d = gov.deflation;
628
611
  lines.push("## Deflation / Fee Burn Metrics");
629
- lines.push(`- **Unallocated PIV This Cycle:** ${Number(deflation.unallocatedPivPerCycle).toLocaleString()} PIV (never minted)`);
630
- lines.push(`- **Annual Unallocated (est.):** ${Number(deflation.annualUnallocatedPiv).toLocaleString()} PIV`);
631
- lines.push(`- **Effective Inflation Reduction:** ${deflation.effectiveInflationReduction}`);
632
- lines.push(`- **Proposal Submission Fee:** ${deflation.proposalFeeBurnPiv} PIV (burned/destroyed)`);
612
+ lines.push(`- **Unallocated PIV This Cycle:** ${d.unallocatedPivPerCycle.toLocaleString()} PIV (never minted)`);
613
+ lines.push(`- **Annual Unallocated (est.):** ${d.annualUnallocatedPiv.toLocaleString()} PIV`);
614
+ lines.push(`- **Effective Inflation Reduction:** ${d.effectiveInflationReduction}`);
615
+ lines.push(`- **Proposal Submission Fee:** ${d.proposalFeeBurnPiv} PIV (burned/destroyed)`);
633
616
  lines.push("");
634
617
  }
635
- // Proposals
636
- let proposals = (data.proposals ?? []);
618
+ let proposals = gov.proposals;
637
619
  if (filter === "passing")
638
620
  proposals = proposals.filter((p) => p.status === "passing");
639
621
  else if (filter === "failing")
@@ -646,11 +628,11 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
646
628
  const fundedTag = p.funded ? " (Funded)" : "";
647
629
  lines.push(`### ${p.name} — ${status}${fundedTag}`);
648
630
  lines.push(`- **Votes:** ${p.yesVotes} Yes / ${p.noVotes} No (${p.netYesPercent}% net yes)`);
649
- lines.push(`- **Monthly Payment:** ${Number(p.monthlyPaymentPiv).toLocaleString()} PIV (~$${Number(p.monthlyPaymentUsd).toLocaleString()})`);
650
- if (Number(p.totalPaymentPiv) > Number(p.monthlyPaymentPiv)) {
651
- lines.push(`- **Total Budget:** ${Number(p.totalPaymentPiv).toLocaleString()} PIV`);
631
+ lines.push(`- **Monthly Payment:** ${p.monthlyPaymentPiv.toLocaleString()} PIV (~$${p.monthlyPaymentUsd.toLocaleString()})`);
632
+ if (p.totalPaymentPiv > p.monthlyPaymentPiv) {
633
+ lines.push(`- **Total Budget:** ${p.totalPaymentPiv.toLocaleString()} PIV`);
652
634
  }
653
- if (Number(p.installmentsRemaining) > 0) {
635
+ if (p.installmentsRemaining > 0) {
654
636
  lines.push(`- **Installments Remaining:** ${p.installmentsRemaining}`);
655
637
  }
656
638
  if (p.budgetPercent)
@@ -663,12 +645,8 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
663
645
  else {
664
646
  lines.push("No proposals found matching the filter.");
665
647
  }
666
- // Meta
667
- const meta = data.meta;
668
- if (meta) {
669
- lines.push("---");
670
- lines.push(`Source: ${meta.source} | ${meta.timestamp}${meta.cacheHit ? " (cached)" : ""}`);
671
- }
648
+ lines.push("---");
649
+ lines.push(`Source: ${gov.source} | ${gov.timestamp}${gov.cacheHit ? " (cached)" : ""}`);
672
650
  return {
673
651
  content: [{ type: "text", text: lines.join("\n") }],
674
652
  };
@@ -677,7 +655,7 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
677
655
  return {
678
656
  content: [{
679
657
  type: "text",
680
- text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}`,
658
+ text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool scrapes pivx.org/proposals directly — the site may be temporarily unreachable.`,
681
659
  }],
682
660
  };
683
661
  }
@@ -0,0 +1,42 @@
1
+ export interface PivxProposal {
2
+ name: string;
3
+ url: string;
4
+ status: "passing" | "failing";
5
+ funded: boolean;
6
+ netYesPercent: number;
7
+ yesVotes: number;
8
+ noVotes: number;
9
+ monthlyPaymentPiv: number;
10
+ monthlyPaymentUsd: number;
11
+ totalPaymentPiv: number;
12
+ installmentsRemaining: number;
13
+ totalInstallments: number;
14
+ budgetPercent: number;
15
+ }
16
+ export interface PivxNetworkStats {
17
+ masternodeCount: number;
18
+ passingThreshold: number;
19
+ monthlyBudgetPiv: number;
20
+ monthlyBudgetUsd: number;
21
+ budgetAllocatedPiv: number;
22
+ budgetAllocatedUsd: number;
23
+ budgetAllocatedPercent: number;
24
+ blockHeight: number;
25
+ totalSupply: number;
26
+ circulatingSupply: number;
27
+ }
28
+ export interface PivxGovernanceData {
29
+ proposals: PivxProposal[];
30
+ network: PivxNetworkStats;
31
+ deflation: {
32
+ unallocatedPivPerCycle: number;
33
+ unallocatedPercent: number;
34
+ annualUnallocatedPiv: number;
35
+ proposalFeeBurnPiv: number;
36
+ effectiveInflationReduction: string;
37
+ };
38
+ timestamp: string;
39
+ source: string;
40
+ cacheHit: boolean;
41
+ }
42
+ export declare function fetchPivxGovernance(): Promise<PivxGovernanceData>;
@@ -0,0 +1,232 @@
1
+ import * as cheerio from "cheerio";
2
+ // 5-minute cache
3
+ const CACHE_TTL_MS = 5 * 60 * 1000;
4
+ let cachedData = null;
5
+ let cacheTimestamp = 0;
6
+ const CHAINZ_BASE = "https://chainz.cryptoid.info/pivx/api.dws";
7
+ async function chainzFetch(query) {
8
+ const res = await fetch(`${CHAINZ_BASE}?q=${query}`, {
9
+ signal: AbortSignal.timeout(8000),
10
+ });
11
+ if (!res.ok)
12
+ throw new Error(`Chainz API ${query}: HTTP ${res.status}`);
13
+ return (await res.text()).trim();
14
+ }
15
+ async function fetchNetworkStats() {
16
+ const [masternodeCount, blockHeight, totalSupply, circulating] = await Promise.allSettled([
17
+ chainzFetch("masternodecount"),
18
+ chainzFetch("getblockcount"),
19
+ chainzFetch("totalcoins"),
20
+ chainzFetch("circulating"),
21
+ ]);
22
+ const mn = masternodeCount.status === "fulfilled"
23
+ ? parseInt(masternodeCount.value, 10)
24
+ : 0;
25
+ const bh = blockHeight.status === "fulfilled"
26
+ ? parseInt(blockHeight.value, 10)
27
+ : 0;
28
+ const ts = totalSupply.status === "fulfilled"
29
+ ? parseFloat(totalSupply.value)
30
+ : 0;
31
+ const cs = circulating.status === "fulfilled"
32
+ ? parseFloat(circulating.value)
33
+ : 0;
34
+ return {
35
+ masternodeCount: isNaN(mn) ? 0 : mn,
36
+ blockHeight: isNaN(bh) ? 0 : bh,
37
+ totalSupply: isNaN(ts) ? 0 : ts,
38
+ circulatingSupply: isNaN(cs) ? 0 : cs,
39
+ passingThreshold: isNaN(mn) ? 0 : Math.ceil(mn * 0.1),
40
+ };
41
+ }
42
+ function parseNumber(text) {
43
+ const cleaned = text.replace(/[^0-9.\-]/g, "");
44
+ const num = parseFloat(cleaned);
45
+ return isNaN(num) ? 0 : num;
46
+ }
47
+ async function scrapePivxProposals() {
48
+ const res = await fetch("https://pivx.org/proposals", {
49
+ signal: AbortSignal.timeout(15000),
50
+ headers: {
51
+ "User-Agent": "MPP32-Governance-Oracle/1.0 (+https://mpp32.org)",
52
+ Accept: "text/html",
53
+ },
54
+ });
55
+ if (!res.ok)
56
+ throw new Error(`pivx.org/proposals returned HTTP ${res.status}`);
57
+ const html = await res.text();
58
+ const $ = cheerio.load(html);
59
+ const pageText = $("body").text();
60
+ let monthlyBudgetPiv = 432000;
61
+ let monthlyBudgetUsd = 0;
62
+ let budgetAllocatedPiv = 0;
63
+ let budgetAllocatedUsd = 0;
64
+ let masternodeCount = 0;
65
+ let passingThreshold = 0;
66
+ const budgetMatch = pageText.match(/Monthly\s*Budget[:\s]*([\d,]+)\s*PIV/i);
67
+ if (budgetMatch?.[1])
68
+ monthlyBudgetPiv = parseNumber(budgetMatch[1]);
69
+ const budgetUsdMatch = pageText.match(/Monthly\s*Budget[^$]*US?\$([\d,.]+)/i);
70
+ if (budgetUsdMatch?.[1])
71
+ monthlyBudgetUsd = parseNumber(budgetUsdMatch[1]);
72
+ const allocatedMatch = pageText.match(/Budget\s*Allocated[:\s]*([\d,]+)\s*PIV/i);
73
+ if (allocatedMatch?.[1])
74
+ budgetAllocatedPiv = parseNumber(allocatedMatch[1]);
75
+ const allocatedUsdMatch = pageText.match(/Budget\s*Allocated[^$]*US?\$([\d,.]+)/i);
76
+ if (allocatedUsdMatch?.[1])
77
+ budgetAllocatedUsd = parseNumber(allocatedUsdMatch[1]);
78
+ const mnMatch = pageText.match(/([\d,]+)\s*masternodes?\s*online/i);
79
+ if (mnMatch?.[1])
80
+ masternodeCount = parseNumber(mnMatch[1]);
81
+ const thresholdMatch = pageText.match(/Positive\s*votes\s*required[^:]*:\s*(\d+)/i);
82
+ if (thresholdMatch?.[1])
83
+ passingThreshold = parseInt(thresholdMatch[1], 10);
84
+ const proposals = [];
85
+ const seenHashes = new Set();
86
+ $("table#js_table tbody tr[data-hash]").each((_i, el) => {
87
+ const $row = $(el);
88
+ const hash = $row.attr("data-hash") || "";
89
+ if (!hash || seenHashes.has(hash))
90
+ return;
91
+ seenHashes.add(hash);
92
+ const name = ($row.attr("data-title") || "").trim();
93
+ if (!name)
94
+ return;
95
+ const cells = $row.find("td");
96
+ if (cells.length < 5)
97
+ return;
98
+ const statusCell = $(cells[0]);
99
+ const statusText = statusCell.text();
100
+ const isPassing = /passing/i.test(statusText);
101
+ const funded = /funded/i.test(statusText);
102
+ let netYesPercent = 0;
103
+ const netYesMatch = statusText.match(/([-\d.]+)%/);
104
+ if (netYesMatch?.[1])
105
+ netYesPercent = parseFloat(netYesMatch[1]);
106
+ const nameCell = $(cells[1]);
107
+ const link = nameCell.find("a").first();
108
+ const url = link.attr("href") || "";
109
+ const paymentCell = $(cells[2]);
110
+ const monthlyPaymentPiv = parseNumber(paymentCell.attr("data-piv") || paymentCell.attr("data-order") || "0");
111
+ const budgetPercent = parseFloat(paymentCell.attr("data-percent") || "0");
112
+ let monthlyPaymentUsd = 0;
113
+ const usdSpan = paymentCell.find(".curr-prefix").parent();
114
+ if (usdSpan.length) {
115
+ const usdText = usdSpan.text();
116
+ const usdMatch = usdText.match(/US?\$\s*([\d,]+(?:\.\d+)?)/i);
117
+ if (usdMatch?.[1])
118
+ monthlyPaymentUsd = parseNumber(usdMatch[1]);
119
+ }
120
+ let installmentsRemaining = 1;
121
+ const longLine = paymentCell.find(".long-line");
122
+ if (longLine.length) {
123
+ const installB = longLine.find("b").first();
124
+ if (installB.length) {
125
+ const n = parseInt(installB.text().trim(), 10);
126
+ if (!isNaN(n))
127
+ installmentsRemaining = n;
128
+ }
129
+ }
130
+ let totalPaymentPiv = monthlyPaymentPiv;
131
+ if (longLine.length) {
132
+ const totalB = longLine.find("b").last();
133
+ if (totalB.length) {
134
+ const totalText = totalB.text();
135
+ const totalMatch = totalText.match(/([\d,]+(?:\.\d+)?)\s*PIV/i);
136
+ if (totalMatch?.[1])
137
+ totalPaymentPiv = parseNumber(totalMatch[1]);
138
+ }
139
+ }
140
+ const voteCell = $(cells[4]);
141
+ const voteText = voteCell.text();
142
+ let yesVotes = 0;
143
+ let noVotes = 0;
144
+ const voteMatch = voteText.match(/(\d+)\s*\/\s*(\d+)/);
145
+ if (voteMatch?.[1] && voteMatch[2]) {
146
+ yesVotes = parseInt(voteMatch[1], 10);
147
+ noVotes = parseInt(voteMatch[2], 10);
148
+ }
149
+ proposals.push({
150
+ name,
151
+ url,
152
+ status: isPassing ? "passing" : "failing",
153
+ funded,
154
+ netYesPercent,
155
+ yesVotes,
156
+ noVotes,
157
+ monthlyPaymentPiv,
158
+ monthlyPaymentUsd,
159
+ totalPaymentPiv,
160
+ installmentsRemaining,
161
+ totalInstallments: installmentsRemaining,
162
+ budgetPercent,
163
+ });
164
+ });
165
+ return {
166
+ proposals,
167
+ budgetSummary: {
168
+ monthlyBudgetPiv,
169
+ monthlyBudgetUsd,
170
+ budgetAllocatedPiv,
171
+ budgetAllocatedUsd,
172
+ masternodeCount,
173
+ passingThreshold,
174
+ },
175
+ };
176
+ }
177
+ export async function fetchPivxGovernance() {
178
+ if (cachedData && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
179
+ return { ...cachedData, cacheHit: true };
180
+ }
181
+ const [scrapeResult, networkStats] = await Promise.allSettled([
182
+ scrapePivxProposals(),
183
+ fetchNetworkStats(),
184
+ ]);
185
+ const scraped = scrapeResult.status === "fulfilled" ? scrapeResult.value : null;
186
+ const chainzStats = networkStats.status === "fulfilled" ? networkStats.value : {};
187
+ if (!scraped) {
188
+ throw new Error(`Failed to fetch PIVX governance data: ${scrapeResult.status === "rejected" ? scrapeResult.reason : "unknown"}`);
189
+ }
190
+ const budgetAllocatedPercent = scraped.budgetSummary.monthlyBudgetPiv > 0
191
+ ? Math.round((scraped.budgetSummary.budgetAllocatedPiv /
192
+ scraped.budgetSummary.monthlyBudgetPiv) *
193
+ 10000) / 100
194
+ : 0;
195
+ const network = {
196
+ masternodeCount: scraped.budgetSummary.masternodeCount ||
197
+ chainzStats.masternodeCount ||
198
+ 0,
199
+ passingThreshold: scraped.budgetSummary.passingThreshold ||
200
+ chainzStats.passingThreshold ||
201
+ 0,
202
+ monthlyBudgetPiv: scraped.budgetSummary.monthlyBudgetPiv,
203
+ monthlyBudgetUsd: scraped.budgetSummary.monthlyBudgetUsd,
204
+ budgetAllocatedPiv: scraped.budgetSummary.budgetAllocatedPiv,
205
+ budgetAllocatedUsd: scraped.budgetSummary.budgetAllocatedUsd,
206
+ budgetAllocatedPercent,
207
+ blockHeight: chainzStats.blockHeight || 0,
208
+ totalSupply: chainzStats.totalSupply || 0,
209
+ circulatingSupply: chainzStats.circulatingSupply || 0,
210
+ };
211
+ const unallocatedPivPerCycle = network.monthlyBudgetPiv - network.budgetAllocatedPiv;
212
+ const annualUnallocatedPiv = unallocatedPivPerCycle * 12;
213
+ const data = {
214
+ proposals: scraped.proposals,
215
+ network,
216
+ deflation: {
217
+ unallocatedPivPerCycle,
218
+ unallocatedPercent: 100 - budgetAllocatedPercent,
219
+ annualUnallocatedPiv,
220
+ proposalFeeBurnPiv: 50,
221
+ effectiveInflationReduction: network.totalSupply > 0
222
+ ? `${((annualUnallocatedPiv / network.totalSupply) * 100).toFixed(3)}%`
223
+ : "N/A",
224
+ },
225
+ timestamp: new Date().toISOString(),
226
+ source: "pivx.org/proposals + chainz.cryptoid.info",
227
+ cacheHit: false,
228
+ };
229
+ cachedData = data;
230
+ cacheTimestamp = Date.now();
231
+ return data;
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "mcpName": "io.github.MPP32/mpp32-mcp-server",
5
5
  "description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
6
6
  "type": "module",
@@ -76,6 +76,7 @@
76
76
  "@solana-program/token": "^0.13.0",
77
77
  "@solana/kit": "^6.9.0",
78
78
  "bs58": "^6.0.0",
79
+ "cheerio": "^1.2.0",
79
80
  "tweetnacl": "^1.0.3",
80
81
  "viem": "^2.48.11",
81
82
  "zod": "^3.23.0"