mj41-mcp 1.1.0 → 1.2.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 +90 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,43 @@
|
|
|
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
|
+
};
|
|
26
63
|
const COPPERTRACE_URL = "https://coppertrace.vercel.app";
|
|
27
64
|
const FIRST_SIGNAL_URL = "https://aiwire.mj41.me";
|
|
28
65
|
const ORACLE_URLS = {
|
|
@@ -46,11 +83,14 @@ server.tool("get_copper_price", "Get the current copper price from a ZJ Industri
|
|
|
46
83
|
.describe("Copper class: 'a' for COMEX spot, 'b' for scrap, 'c' for industrial"),
|
|
47
84
|
}, async ({ class: copperClass }) => {
|
|
48
85
|
try {
|
|
86
|
+
checkRate("copper", RATE.COPPER);
|
|
49
87
|
const url = ORACLE_URLS[copperClass];
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
88
|
+
const status = await cachedFetch(`copper-status-${copperClass}`, async () => {
|
|
89
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
throw new Error(`HTTP ${res.status}`);
|
|
92
|
+
return res.json();
|
|
93
|
+
}, TTL.COPPER_STATUS);
|
|
54
94
|
const label = CLASS_LABELS[copperClass];
|
|
55
95
|
const fresh = status.is_fresh ? "FRESH" : "STALE";
|
|
56
96
|
const lastUpdate = new Date(status.last_updated).toLocaleString("en-US", {
|
|
@@ -97,10 +137,13 @@ server.tool("get_all_copper_prices", "Get status of all three ZJ Industries copp
|
|
|
97
137
|
"═══════════════════════════════════════════════",
|
|
98
138
|
"",
|
|
99
139
|
];
|
|
140
|
+
checkRate("copper", RATE.COPPER);
|
|
100
141
|
for (const [cls, url] of Object.entries(ORACLE_URLS)) {
|
|
101
142
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
143
|
+
const status = await cachedFetch(`copper-status-${cls}`, async () => {
|
|
144
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
145
|
+
return res.json();
|
|
146
|
+
}, TTL.COPPER_STATUS);
|
|
104
147
|
const fresh = status.is_fresh ? "FRESH" : "STALE";
|
|
105
148
|
const lastUpdate = new Date(status.last_updated).toLocaleString("en-US", {
|
|
106
149
|
timeZone: "America/New_York",
|
|
@@ -138,11 +181,14 @@ server.tool("get_oracle_status", "Check if a ZJ Industries copper oracle is oper
|
|
|
138
181
|
.describe("Copper class: 'a' for COMEX spot, 'b' for scrap, 'c' for industrial"),
|
|
139
182
|
}, async ({ class: copperClass }) => {
|
|
140
183
|
try {
|
|
184
|
+
checkRate("copper", RATE.COPPER);
|
|
141
185
|
const url = ORACLE_URLS[copperClass];
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
186
|
+
const data = await cachedFetch(`copper-status-${copperClass}`, async () => {
|
|
187
|
+
const res = await fetch(`${url}/api/v1/status`);
|
|
188
|
+
if (!res.ok)
|
|
189
|
+
throw new Error(`HTTP ${res.status}`);
|
|
190
|
+
return res.json();
|
|
191
|
+
}, TTL.COPPER_STATUS);
|
|
146
192
|
return {
|
|
147
193
|
content: [
|
|
148
194
|
{
|
|
@@ -169,6 +215,8 @@ server.tool("investigate_wallet", "Investigate a blockchain wallet address or tr
|
|
|
169
215
|
.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
216
|
}, async ({ query }) => {
|
|
171
217
|
try {
|
|
218
|
+
checkRate("richard", RATE.RICHARD);
|
|
219
|
+
// No cache — investigations are unique per query
|
|
172
220
|
const res = await fetch(`${COPPERTRACE_URL}/api/investigate`, {
|
|
173
221
|
method: "POST",
|
|
174
222
|
headers: { "Content-Type": "application/json" },
|
|
@@ -205,8 +253,11 @@ server.tool("investigate_wallet", "Investigate a blockchain wallet address or tr
|
|
|
205
253
|
// ── get_eth_price ───────────────────────────────────────────
|
|
206
254
|
server.tool("get_eth_price", "Get the current price of Ethereum in USD. Powered by mj41, LLC.", {}, async () => {
|
|
207
255
|
try {
|
|
208
|
-
|
|
209
|
-
const data = await
|
|
256
|
+
checkRate("richard", RATE.RICHARD);
|
|
257
|
+
const data = await cachedFetch("eth-price", async () => {
|
|
258
|
+
const res = await fetch(`${COPPERTRACE_URL}/api/eth-price`);
|
|
259
|
+
return res.json();
|
|
260
|
+
}, TTL.ETH_PRICE);
|
|
210
261
|
return {
|
|
211
262
|
content: [
|
|
212
263
|
{
|
|
@@ -235,11 +286,14 @@ server.tool("get_latest_news", "Get the latest stories from The First Signal, an
|
|
|
235
286
|
.describe("Number of stories to return (default 5, max 20)"),
|
|
236
287
|
}, async ({ limit }) => {
|
|
237
288
|
try {
|
|
289
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
238
290
|
const cap = Math.min(limit, 20);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
291
|
+
const data = await cachedFetch(`news-latest-${cap}`, async () => {
|
|
292
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?limit=${cap}`);
|
|
293
|
+
if (!res.ok)
|
|
294
|
+
throw new Error(`HTTP ${res.status}`);
|
|
295
|
+
return res.json();
|
|
296
|
+
}, TTL.NEWS_LIST);
|
|
243
297
|
const stories = data.stories || data;
|
|
244
298
|
if (!stories.length) {
|
|
245
299
|
return {
|
|
@@ -259,6 +313,8 @@ server.tool("get_latest_news", "Get the latest stories from The First Signal, an
|
|
|
259
313
|
});
|
|
260
314
|
lines.push(`[${s.beat?.toUpperCase() || "WIRE"}] ${s.headline}`);
|
|
261
315
|
lines.push(` ${date} | ${s.byline || "Staff"} | Confidence: ${s.confidence || "N/A"}`);
|
|
316
|
+
if (s.id)
|
|
317
|
+
lines.push(` ID: ${s.id}`);
|
|
262
318
|
if (s.summary)
|
|
263
319
|
lines.push(` ${s.summary.slice(0, 200)}${s.summary.length > 200 ? "..." : ""}`);
|
|
264
320
|
lines.push("");
|
|
@@ -287,11 +343,14 @@ server.tool("get_news_by_beat", "Get stories from a specific beat on The First S
|
|
|
287
343
|
.describe("Number of stories (default 5)"),
|
|
288
344
|
}, async ({ beat, limit }) => {
|
|
289
345
|
try {
|
|
346
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
290
347
|
const cap = Math.min(limit, 20);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
348
|
+
const data = await cachedFetch(`news-beat-${beat}-${cap}`, async () => {
|
|
349
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories?beat=${beat}&limit=${cap}`);
|
|
350
|
+
if (!res.ok)
|
|
351
|
+
throw new Error(`HTTP ${res.status}`);
|
|
352
|
+
return res.json();
|
|
353
|
+
}, TTL.NEWS_LIST);
|
|
295
354
|
const stories = data.stories || data;
|
|
296
355
|
if (!stories.length) {
|
|
297
356
|
return {
|
|
@@ -310,6 +369,8 @@ server.tool("get_news_by_beat", "Get stories from a specific beat on The First S
|
|
|
310
369
|
});
|
|
311
370
|
lines.push(`${s.headline}`);
|
|
312
371
|
lines.push(` ${date} | ${s.byline || "Staff"} | Confidence: ${s.confidence || "N/A"}`);
|
|
372
|
+
if (s.id)
|
|
373
|
+
lines.push(` ID: ${s.id}`);
|
|
313
374
|
if (s.summary)
|
|
314
375
|
lines.push(` ${s.summary.slice(0, 200)}${s.summary.length > 200 ? "..." : ""}`);
|
|
315
376
|
lines.push("");
|
|
@@ -333,10 +394,13 @@ server.tool("get_story", "Read the full text of a specific story from The First
|
|
|
333
394
|
.describe("The story ID (UUID)"),
|
|
334
395
|
}, async ({ id }) => {
|
|
335
396
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
397
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
398
|
+
const story = await cachedFetch(`news-story-${id}`, async () => {
|
|
399
|
+
const res = await fetch(`${FIRST_SIGNAL_URL}/api/stories/${id}`);
|
|
400
|
+
if (!res.ok)
|
|
401
|
+
throw new Error(`HTTP ${res.status}`);
|
|
402
|
+
return res.json();
|
|
403
|
+
}, TTL.NEWS_STORY);
|
|
340
404
|
const lines = [
|
|
341
405
|
`THE FIRST SIGNAL`,
|
|
342
406
|
"════════════════════════════════",
|
|
@@ -371,6 +435,8 @@ server.tool("submit_story_idea", "Submit a story idea or investigation request t
|
|
|
371
435
|
description: z.string().describe("Details about what should be investigated"),
|
|
372
436
|
}, async ({ topic, description }) => {
|
|
373
437
|
try {
|
|
438
|
+
checkRate("first-signal", RATE.FIRST_SIGNAL);
|
|
439
|
+
// No cache — write operation
|
|
374
440
|
const res = await fetch(`${FIRST_SIGNAL_URL}/api/requests`, {
|
|
375
441
|
method: "POST",
|
|
376
442
|
headers: { "Content-Type": "application/json" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mj41-mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|