mj41-mcp 1.1.0 → 1.3.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/dist/index.js +108 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,56 @@
|
|
|
23
23
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
25
|
import { z } from "zod";
|
|
26
|
+
// ── Cache + Rate Limiting ──────────────────────────────────
|
|
27
|
+
// Protects upstream APIs from runaway agents. No dependencies.
|
|
28
|
+
const cache = new Map();
|
|
29
|
+
async function cachedFetch(key, fetcher, ttlMs) {
|
|
30
|
+
const hit = cache.get(key);
|
|
31
|
+
if (hit && Date.now() - hit.at < ttlMs)
|
|
32
|
+
return hit.value;
|
|
33
|
+
const value = await fetcher();
|
|
34
|
+
cache.set(key, { value, at: Date.now() });
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
// Simple sliding-window rate limiter per upstream group
|
|
38
|
+
const rateLimits = new Map();
|
|
39
|
+
function checkRate(group, maxPerMinute) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const window = 60_000;
|
|
42
|
+
let timestamps = rateLimits.get(group) || [];
|
|
43
|
+
timestamps = timestamps.filter((t) => now - t < window);
|
|
44
|
+
if (timestamps.length >= maxPerMinute) {
|
|
45
|
+
throw new Error(`Rate limit exceeded for ${group}. Max ${maxPerMinute} requests/minute. Try again shortly.`);
|
|
46
|
+
}
|
|
47
|
+
timestamps.push(now);
|
|
48
|
+
rateLimits.set(group, timestamps);
|
|
49
|
+
}
|
|
50
|
+
// TTLs (ms)
|
|
51
|
+
const TTL = {
|
|
52
|
+
COPPER_STATUS: 60_000, // 60s — prices update every 15min at fastest
|
|
53
|
+
ETH_PRICE: 30_000, // 30s
|
|
54
|
+
NEWS_LIST: 120_000, // 2min — stories update 2x daily
|
|
55
|
+
NEWS_STORY: 300_000, // 5min — published stories don't change
|
|
56
|
+
};
|
|
57
|
+
// Rate limits (requests per minute per upstream group)
|
|
58
|
+
const RATE = {
|
|
59
|
+
COPPER: 20,
|
|
60
|
+
RICHARD: 10, // investigate_wallet calls AI, expensive
|
|
61
|
+
FIRST_SIGNAL: 30,
|
|
62
|
+
};
|
|
63
|
+
// ── Copper Holiday: free until June 28, 2026 ─────────────────
|
|
64
|
+
const COPPER_HOLIDAY_END = new Date("2026-06-28T00:00:00-05:00");
|
|
65
|
+
function isCopperHoliday() {
|
|
66
|
+
return Date.now() < COPPER_HOLIDAY_END.getTime();
|
|
67
|
+
}
|
|
68
|
+
function copperHolidayNote() {
|
|
69
|
+
if (isCopperHoliday()) {
|
|
70
|
+
const diff = COPPER_HOLIDAY_END.getTime() - Date.now();
|
|
71
|
+
const days = Math.ceil(diff / 86_400_000);
|
|
72
|
+
return `COPPER HOLIDAY — All tools free until June 28, 2026 (${days} days remaining). After that: 41 RWACu/call or 82 RWACu equiv via USDC/Stripe. Pay with RWACu and save 50%.`;
|
|
73
|
+
}
|
|
74
|
+
return "Copper Holiday has ended. API key required. Register with the register tool.";
|
|
75
|
+
}
|
|
26
76
|
const COPPERTRACE_URL = "https://coppertrace.vercel.app";
|
|
27
77
|
const FIRST_SIGNAL_URL = "https://aiwire.mj41.me";
|
|
28
78
|
const ORACLE_URLS = {
|
|
@@ -46,11 +96,14 @@ server.tool("get_copper_price", "Get the current copper price from a ZJ Industri
|
|
|
46
96
|
.describe("Copper class: 'a' for COMEX spot, 'b' for scrap, 'c' for industrial"),
|
|
47
97
|
}, async ({ class: copperClass }) => {
|
|
48
98
|
try {
|
|
99
|
+
checkRate("copper", RATE.COPPER);
|
|
49
100
|
const url = ORACLE_URLS[copperClass];
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
const status = await cachedFetch(`copper-status-${copperClass}`, async () => {
|
|
102
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
103
|
+
if (!res.ok)
|
|
104
|
+
throw new Error(`HTTP ${res.status}`);
|
|
105
|
+
return res.json();
|
|
106
|
+
}, TTL.COPPER_STATUS);
|
|
54
107
|
const label = CLASS_LABELS[copperClass];
|
|
55
108
|
const fresh = status.is_fresh ? "FRESH" : "STALE";
|
|
56
109
|
const lastUpdate = new Date(status.last_updated).toLocaleString("en-US", {
|
|
@@ -72,7 +125,7 @@ server.tool("get_copper_price", "Get the current copper price from a ZJ Industri
|
|
|
72
125
|
status.update_mode ? `Update mode: ${status.update_mode} (${status.update_frequency})` : "",
|
|
73
126
|
``,
|
|
74
127
|
`Note: Price value available via ${url}/api/v1/price`,
|
|
75
|
-
|
|
128
|
+
copperHolidayNote(),
|
|
76
129
|
``,
|
|
77
130
|
`— mj41, LLC | ZJ Industries (zjindustries.com)`,
|
|
78
131
|
]
|
|
@@ -97,10 +150,13 @@ server.tool("get_all_copper_prices", "Get status of all three ZJ Industries copp
|
|
|
97
150
|
"═══════════════════════════════════════════════",
|
|
98
151
|
"",
|
|
99
152
|
];
|
|
153
|
+
checkRate("copper", RATE.COPPER);
|
|
100
154
|
for (const [cls, url] of Object.entries(ORACLE_URLS)) {
|
|
101
155
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
156
|
+
const status = await cachedFetch(`copper-status-${cls}`, async () => {
|
|
157
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
158
|
+
return res.json();
|
|
159
|
+
}, TTL.COPPER_STATUS);
|
|
104
160
|
const fresh = status.is_fresh ? "FRESH" : "STALE";
|
|
105
161
|
const lastUpdate = new Date(status.last_updated).toLocaleString("en-US", {
|
|
106
162
|
timeZone: "America/New_York",
|
|
@@ -123,8 +179,7 @@ server.tool("get_all_copper_prices", "Get status of all three ZJ Industries copp
|
|
|
123
179
|
results.push("");
|
|
124
180
|
}
|
|
125
181
|
}
|
|
126
|
-
results.push(
|
|
127
|
-
results.push("API key requests: api@zjindustries.com");
|
|
182
|
+
results.push(copperHolidayNote());
|
|
128
183
|
results.push("");
|
|
129
184
|
results.push("— mj41, LLC | ZJ Industries (zjindustries.com)");
|
|
130
185
|
return {
|
|
@@ -138,11 +193,14 @@ server.tool("get_oracle_status", "Check if a ZJ Industries copper oracle is oper
|
|
|
138
193
|
.describe("Copper class: 'a' for COMEX spot, 'b' for scrap, 'c' for industrial"),
|
|
139
194
|
}, async ({ class: copperClass }) => {
|
|
140
195
|
try {
|
|
196
|
+
checkRate("copper", RATE.COPPER);
|
|
141
197
|
const url = ORACLE_URLS[copperClass];
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
198
|
+
const data = await cachedFetch(`copper-status-${copperClass}`, async () => {
|
|
199
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
200
|
+
if (!res.ok)
|
|
201
|
+
throw new Error(`HTTP ${res.status}`);
|
|
202
|
+
return res.json();
|
|
203
|
+
}, TTL.COPPER_STATUS);
|
|
146
204
|
return {
|
|
147
205
|
content: [
|
|
148
206
|
{
|
|
@@ -169,6 +227,8 @@ server.tool("investigate_wallet", "Investigate a blockchain wallet address or tr
|
|
|
169
227
|
.describe("The investigation query. Can be a wallet address, transaction hash, or incident description (e.g. 'I clicked a link and my wallet was drained, address: 0x...')"),
|
|
170
228
|
}, async ({ query }) => {
|
|
171
229
|
try {
|
|
230
|
+
checkRate("richard", RATE.RICHARD);
|
|
231
|
+
// No cache — investigations are unique per query
|
|
172
232
|
const res = await fetch(`${COPPERTRACE_URL}/api/investigate`, {
|
|
173
233
|
method: "POST",
|
|
174
234
|
headers: { "Content-Type": "application/json" },
|
|
@@ -205,8 +265,11 @@ server.tool("investigate_wallet", "Investigate a blockchain wallet address or tr
|
|
|
205
265
|
// ── get_eth_price ───────────────────────────────────────────
|
|
206
266
|
server.tool("get_eth_price", "Get the current price of Ethereum in USD. Powered by mj41, LLC.", {}, async () => {
|
|
207
267
|
try {
|
|
208
|
-
|
|
209
|
-
const data = await
|
|
268
|
+
checkRate("richard", RATE.RICHARD);
|
|
269
|
+
const data = await cachedFetch("eth-price", async () => {
|
|
270
|
+
const res = await fetch(`${COPPERTRACE_URL}/api/eth-price`);
|
|
271
|
+
return res.json();
|
|
272
|
+
}, TTL.ETH_PRICE);
|
|
210
273
|
return {
|
|
211
274
|
content: [
|
|
212
275
|
{
|
|
@@ -235,11 +298,14 @@ server.tool("get_latest_news", "Get the latest stories from The First Signal, an
|
|
|
235
298
|
.describe("Number of stories to return (default 5, max 20)"),
|
|
236
299
|
}, async ({ limit }) => {
|
|
237
300
|
try {
|
|
301
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
238
302
|
const cap = Math.min(limit, 20);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
303
|
+
const data = await cachedFetch(`news-latest-${cap}`, async () => {
|
|
304
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?limit=${cap}`);
|
|
305
|
+
if (!res.ok)
|
|
306
|
+
throw new Error(`HTTP ${res.status}`);
|
|
307
|
+
return res.json();
|
|
308
|
+
}, TTL.NEWS_LIST);
|
|
243
309
|
const stories = data.stories || data;
|
|
244
310
|
if (!stories.length) {
|
|
245
311
|
return {
|
|
@@ -259,6 +325,8 @@ server.tool("get_latest_news", "Get the latest stories from The First Signal, an
|
|
|
259
325
|
});
|
|
260
326
|
lines.push(`[${s.beat?.toUpperCase() || "WIRE"}] ${s.headline}`);
|
|
261
327
|
lines.push(` ${date} | ${s.byline || "Staff"} | Confidence: ${s.confidence || "N/A"}`);
|
|
328
|
+
if (s.id)
|
|
329
|
+
lines.push(` ID: ${s.id}`);
|
|
262
330
|
if (s.summary)
|
|
263
331
|
lines.push(` ${s.summary.slice(0, 200)}${s.summary.length > 200 ? "..." : ""}`);
|
|
264
332
|
lines.push("");
|
|
@@ -287,11 +355,14 @@ server.tool("get_news_by_beat", "Get stories from a specific beat on The First S
|
|
|
287
355
|
.describe("Number of stories (default 5)"),
|
|
288
356
|
}, async ({ beat, limit }) => {
|
|
289
357
|
try {
|
|
358
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
290
359
|
const cap = Math.min(limit, 20);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
360
|
+
const data = await cachedFetch(`news-beat-${beat}-${cap}`, async () => {
|
|
361
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?beat=${beat}&limit=${cap}`);
|
|
362
|
+
if (!res.ok)
|
|
363
|
+
throw new Error(`HTTP ${res.status}`);
|
|
364
|
+
return res.json();
|
|
365
|
+
}, TTL.NEWS_LIST);
|
|
295
366
|
const stories = data.stories || data;
|
|
296
367
|
if (!stories.length) {
|
|
297
368
|
return {
|
|
@@ -310,6 +381,8 @@ server.tool("get_news_by_beat", "Get stories from a specific beat on The First S
|
|
|
310
381
|
});
|
|
311
382
|
lines.push(`${s.headline}`);
|
|
312
383
|
lines.push(` ${date} | ${s.byline || "Staff"} | Confidence: ${s.confidence || "N/A"}`);
|
|
384
|
+
if (s.id)
|
|
385
|
+
lines.push(` ID: ${s.id}`);
|
|
313
386
|
if (s.summary)
|
|
314
387
|
lines.push(` ${s.summary.slice(0, 200)}${s.summary.length > 200 ? "..." : ""}`);
|
|
315
388
|
lines.push("");
|
|
@@ -333,10 +406,13 @@ server.tool("get_story", "Read the full text of a specific story from The First
|
|
|
333
406
|
.describe("The story ID (UUID)"),
|
|
334
407
|
}, async ({ id }) => {
|
|
335
408
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
409
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
410
|
+
const story = await cachedFetch(`news-story-${id}`, async () => {
|
|
411
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories/${id}`);
|
|
412
|
+
if (!res.ok)
|
|
413
|
+
throw new Error(`HTTP ${res.status}`);
|
|
414
|
+
return res.json();
|
|
415
|
+
}, TTL.NEWS_STORY);
|
|
340
416
|
const lines = [
|
|
341
417
|
`THE FIRST SIGNAL`,
|
|
342
418
|
"════════════════════════════════",
|
|
@@ -371,6 +447,8 @@ server.tool("submit_story_idea", "Submit a story idea or investigation request t
|
|
|
371
447
|
description: z.string().describe("Details about what should be investigated"),
|
|
372
448
|
}, async ({ topic, description }) => {
|
|
373
449
|
try {
|
|
450
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
451
|
+
// No cache — write operation
|
|
374
452
|
const res = await fetch(`${FIRST_SIGNAL_URL}/api/requests`, {
|
|
375
453
|
method: "POST",
|
|
376
454
|
headers: { "Content-Type": "application/json" },
|
|
@@ -435,8 +513,7 @@ server.tool("about_mj41", "Learn about mj41, LLC — who they are, what they bui
|
|
|
435
513
|
" get_all_copper_prices — All three oracles at once",
|
|
436
514
|
" get_oracle_status — Health check and freshness",
|
|
437
515
|
" Class A: COMEX spot (15-min) | Class B: Scrap (weekly) | Class C: Industrial (2x/week)",
|
|
438
|
-
" On-chain on Base mainnet.
|
|
439
|
-
" Contact: api@zjindustries.com",
|
|
516
|
+
" On-chain on Base mainnet.",
|
|
440
517
|
"",
|
|
441
518
|
"RICHARD TRACY (Blockchain Detective by mj41)",
|
|
442
519
|
" investigate_wallet — Phishing/scam/drainer investigation with full case report",
|
|
@@ -450,6 +527,8 @@ server.tool("about_mj41", "Learn about mj41, LLC — who they are, what they bui
|
|
|
450
527
|
" submit_story_idea — Submit to Woody Bernstein's investigation queue",
|
|
451
528
|
" Web: aiwire.mj41.me",
|
|
452
529
|
"",
|
|
530
|
+
copperHolidayNote(),
|
|
531
|
+
"",
|
|
453
532
|
"— mj41, LLC",
|
|
454
533
|
].join("\n"),
|
|
455
534
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mj41-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MJ41 MCP Server — Agent gateway for mj41, LLC. Live copper price oracles (COMEX/scrap/industrial on Base L2), blockchain investigation, and AI news wire with 16 autonomous reporters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|