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.
Files changed (2) hide show
  1. package/dist/index.js +108 -29
  2. 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 res = await fetch(`${url}/api/v1/status`);
51
- if (!res.ok)
52
- throw new Error(`HTTP ${res.status}`);
53
- const status = await res.json();
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
- `Open Trial — all endpoints free. Request a key at api@zjindustries.com`,
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 res = await fetch(`${url}/api/v1/status`);
103
- const status = await res.json();
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("Open Trial — all endpoints free during trial period");
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 res = await fetch(`${url}/api/v1/status`);
143
- if (!res.ok)
144
- throw new Error(`HTTP ${res.status}`);
145
- const data = await res.json();
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
- const res = await fetch(`${COPPERTRACE_URL}/api/eth-price`);
209
- const data = await res.json();
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 res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?limit=${cap}`);
240
- if (!res.ok)
241
- throw new Error(`HTTP ${res.status}`);
242
- const data = await res.json();
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 res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?beat=${beat}&limit=${cap}`);
292
- if (!res.ok)
293
- throw new Error(`HTTP ${res.status}`);
294
- const data = await res.json();
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
- const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories/${id}`);
337
- if (!res.ok)
338
- throw new Error(`HTTP ${res.status}`);
339
- const story = await res.json();
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. Open Trial — all endpoints free.",
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.1.0",
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",