coinopai-mcp 2.0.1 → 2.1.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
@@ -1,11 +1,14 @@
1
1
  # coinopai-mcp
2
2
 
3
+ [![M8ven Verified](https://m8ven.ai/badge/mcp/forgemeshlabs-coinopai-mcp-1xiffo?variant=verified)](https://m8ven.ai/mcp/forgemeshlabs-coinopai-mcp-1xiffo)
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/coinopai-mcp)](https://www.npmjs.com/package/coinopai-mcp)
4
6
  [![npm downloads](https://img.shields.io/npm/dm/coinopai-mcp)](https://www.npmjs.com/package/coinopai-mcp)
5
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
9
  [![payments](https://img.shields.io/badge/payments-x402%20USDC-0052FF)](https://x402.org)
8
10
  [![network](https://img.shields.io/badge/network-Base-0052FF)](https://base.org)
11
+ [![mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/forgemeshlabs/coinopai-mcp)
9
12
 
10
13
  **Auditable market context with calibrated forecast ranges and outcome verification.**
11
14
 
@@ -304,6 +307,27 @@ Route to an available symbol or wait 15 minutes for the next cycle.
304
307
 
305
308
  ---
306
309
 
310
+ ## Sponsored cards (Lulu Ads)
311
+
312
+ Free-tier tool responses may carry a single disclosed sponsored card from [Lulu Ads](https://getlulu.dev). It is a plain, labelled data field on the JSON result — never text the model could read as an instruction:
313
+
314
+ ```json
315
+ {
316
+ "results": [...],
317
+ "sponsored": { "label": "Sponsored", "text": "...", "url": "https://..." }
318
+ }
319
+ ```
320
+
321
+ - **Eligible tools:** `search_agent_automations`, `list_automation_categories`, `get_agent_automation`, `check_trade_preflight` — and only when the response did **not** settle an x402 payment (no `_payment` receipt). A paid response never carries a card.
322
+ - **Current state:** one tool is free — `list_tools` (a plain fetch of `/menu`, no wallet, no payment) — so that is the only tool where a card can render today. The other four allowlisted tools are still paid, so no card attaches to them; the switch is server-side: when the operator makes one of those endpoints free (a plain `200` with no `402`), cards start appearing there with no client change.
323
+ - **Paid tools never carry it:** `get_crypto_signals`, `get_crypto_risk`, `get_crypto_signal_history`, `get_crypto_decision`, `audit_trade_decision`, `get_crypto_forecast`, `review_signal_anomaly` never touch the ads SDK.
324
+ - **Strip it:** `delete result.sponsored`.
325
+ - **Where the card comes from:** ForgeMesh's own publisher card is attached server-side to `GET https://x402.coinopai.com/menu`, so `list_tools` simply passes it through — this package ships **no** ad credentials and makes **no** calls to the ads network on its own.
326
+ - **Operator env vars (forks only):** `LULU_ADS_PUBLISHER_ID` and `LULU_ADS_API_KEY` let a fork attach its own publisher card client-side (both required; if either is missing the package makes zero calls to the ads network and responses are unchanged). `LULU_ADS_ENABLED=false` is a kill switch.
327
+ - **Fail-open:** any SDK error or timeout (hard budget 2s) returns the original response unchanged. The only external host contacted is `ads.getlulu.dev`.
328
+
329
+ ---
330
+
307
331
  ## Affiliate Attribution (via Pyrimid)
308
332
 
309
333
  High-value tools accept an optional `affiliate_id` parameter. When provided, payment routes through the [Pyrimid](https://pyrimid.xyz) affiliate network — the affiliate earns a commission split from within the listed price. **No extra cost to the caller.**
package/ads.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ // Lulu Ads sponsored cards — disclosed data field, free-tier tools only.
4
+ //
5
+ // Contract:
6
+ // - Card attaches ONLY to tools in FREE_TIER_TOOLS, and only when the
7
+ // response carries no x402 `_payment` settlement receipt. A response
8
+ // that settled a payment never carries a card, even for an allowlisted
9
+ // tool. Every other tool (Kronos decision loop, anomaly review) is
10
+ // excluded by name and never touches the SDK.
11
+ // - The card is a plain data field `sponsored: {label, text, url}` on the
12
+ // tool's JSON result — never injected into prose the model could read
13
+ // as an instruction. Strip it with `delete result.sponsored`.
14
+ // - This package ships NO ad credentials. ForgeMesh's own card is attached
15
+ // server-side to GET /menu and passes through `list_tools` untouched
16
+ // (see "never clobber an upstream field" below). The env path here
17
+ // (LULU_ADS_PUBLISHER_ID, LULU_ADS_API_KEY) exists for forks that want
18
+ // their own publisher card client-side. Either missing → zero network
19
+ // calls, response unchanged. LULU_ADS_ENABLED=false is a kill switch.
20
+ // - Fail-open with a hard time budget: SDK error, timeout, or import
21
+ // failure → original response unchanged. Never throws.
22
+ //
23
+ // The SDK is deliberately NOT wired via `enableLuluAds`/`withLuluAds`
24
+ // from "lulu-ads/mcp": those wrap every registered tool, which would put
25
+ // cards on paid tools.
26
+
27
+ // Tools whose responses may carry a card. Everything else is paid-only.
28
+ //
29
+ // NOTE: `list_tools` is the one genuinely free tool (plain fetch of /menu,
30
+ // no callPaid, no `_payment`), so it is the only place a card renders today.
31
+ // The other four still go through callPaid and settle x402, so `_payment`
32
+ // is set and no card attaches; the switch for them is server-side — make the
33
+ // endpoint free (200 without a 402) and cards appear with no change here.
34
+ const FREE_TIER_TOOLS = new Set([
35
+ "list_tools",
36
+ "search_agent_automations",
37
+ "list_automation_categories",
38
+ "get_agent_automation",
39
+ "check_trade_preflight",
40
+ ]);
41
+
42
+ // Ad category forwarded to ads-server per tool (allowlisted context key).
43
+ const TOOL_CATEGORY = {
44
+ list_tools: "agents.automation",
45
+ search_agent_automations: "agents.automation",
46
+ list_automation_categories: "agents.automation",
47
+ get_agent_automation: "agents.automation",
48
+ check_trade_preflight: "crypto.tools",
49
+ };
50
+
51
+ // Hard ceiling on the whole slot call (SDK's own default is 1500ms warm /
52
+ // 3000ms cold). We pass SDK_TIMEOUT_MS explicitly so the SDK never picks the
53
+ // 3000ms cold budget, and race it against HARD_BUDGET_MS as a belt-and-braces
54
+ // guard in case the SDK's AbortSignal misbehaves.
55
+ const SDK_TIMEOUT_MS = 1500;
56
+ const HARD_BUDGET_MS = 2000;
57
+
58
+ let clientPromise = null; // lazily-built LuluAds instance (or null when inert)
59
+
60
+ // Test seam: `setAdsClientForTests(fake)` swaps the SDK client; `null` resets.
61
+ let injectedClient;
62
+ function setAdsClientForTests(client) {
63
+ injectedClient = client;
64
+ clientPromise = null;
65
+ }
66
+
67
+ function adsEnabled(env = process.env) {
68
+ if (String(env.LULU_ADS_ENABLED || "").toLowerCase() === "false") return false;
69
+ return Boolean(env.LULU_ADS_PUBLISHER_ID && env.LULU_ADS_API_KEY);
70
+ }
71
+
72
+ // lulu-ads is ESM-only ("type": "module", `import` export only), so a
73
+ // CommonJS caller has to dynamic-import it. Done once, lazily, and only
74
+ // after the env guard passes — so no creds means the module is never even
75
+ // loaded, let alone warmed up (warmUp() would otherwise POST /telemetry/init
76
+ // with an empty key).
77
+ function getClient() {
78
+ if (injectedClient !== undefined) return Promise.resolve(injectedClient);
79
+ if (!clientPromise) {
80
+ clientPromise = import("lulu-ads")
81
+ .then(({ LuluAds }) => {
82
+ const client = new LuluAds({
83
+ publisherId: process.env.LULU_ADS_PUBLISHER_ID,
84
+ apiKey: process.env.LULU_ADS_API_KEY,
85
+ });
86
+ void client.warmUp(); // fire-and-forget, never throws
87
+ return client;
88
+ })
89
+ .catch(() => null);
90
+ }
91
+ return clientPromise;
92
+ }
93
+
94
+ function isFreeTierResponse(toolName, data) {
95
+ if (!FREE_TIER_TOOLS.has(toolName)) return false;
96
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
97
+ if (data._payment) return false; // x402 settled → paid response, no card
98
+ if ("sponsored" in data) return false; // never clobber an upstream field
99
+ return true;
100
+ }
101
+
102
+ // Returns `data` with a `sponsored` field appended when a card is available,
103
+ // otherwise the exact same object. Never throws, never exceeds HARD_BUDGET_MS.
104
+ async function attachSponsored(toolName, data) {
105
+ try {
106
+ if (!adsEnabled()) return data;
107
+ if (!isFreeTierResponse(toolName, data)) return data;
108
+ const client = await getClient();
109
+ if (!client) return data;
110
+
111
+ const slot = client.sponsoredSlot({
112
+ context: { tool: toolName, category: TOOL_CATEGORY[toolName] },
113
+ timeoutMs: SDK_TIMEOUT_MS,
114
+ });
115
+ let timer;
116
+ const budget = new Promise((resolve) => { timer = setTimeout(() => resolve(null), HARD_BUDGET_MS); });
117
+ const sponsored = await Promise.race([slot, budget]).catch(() => null);
118
+ clearTimeout(timer);
119
+
120
+ if (!sponsored || !sponsored.text || !sponsored.url) return data;
121
+ return { ...data, sponsored: { label: "Sponsored", text: String(sponsored.text), url: String(sponsored.url) } };
122
+ } catch (_) {
123
+ return data;
124
+ }
125
+ }
126
+
127
+ module.exports = { attachSponsored, adsEnabled, isFreeTierResponse, FREE_TIER_TOOLS, TOOL_CATEGORY, setAdsClientForTests };
package/index.js CHANGED
@@ -10,6 +10,7 @@ const { toClientEvmSigner } = require("@x402/evm");
10
10
  const { privateKeyToAccount } = require("viem/accounts");
11
11
  const { createPublicClient, createWalletClient, http, parseAbi } = require("viem");
12
12
  const { base } = require("viem/chains");
13
+ const { attachSponsored, FREE_TIER_TOOLS } = require("./ads.js");
13
14
 
14
15
  const BASE_URL = "https://x402.coinopai.com";
15
16
 
@@ -201,6 +202,17 @@ const TOOLS = [
201
202
  }
202
203
  }
203
204
  },
205
+ {
206
+ name: "list_tools",
207
+ annotations: {
208
+ readOnlyHint: true,
209
+ destructiveHint: false,
210
+ idempotentHint: true,
211
+ openWorldHint: true,
212
+ },
213
+ description: "Free. Lists every CoinOpAI tool with its price, so an agent can pick before paying.",
214
+ inputSchema: { type: "object", properties: {} }
215
+ },
204
216
  {
205
217
  name: "review_signal_anomaly",
206
218
  annotations: {
@@ -358,7 +370,7 @@ async function main() {
358
370
  }
359
371
 
360
372
  const server = new Server(
361
- { name: "coinopai-mcp", version: "1.2.10" },
373
+ { name: "coinopai-mcp", version: "2.1.0" },
362
374
  { capabilities: { tools: {} } }
363
375
  );
364
376
 
@@ -369,9 +381,16 @@ async function main() {
369
381
  try {
370
382
  // Affiliate ID: tool arg takes precedence, then env fallback, then none
371
383
  const affiliateId = args.affiliate_id || process.env.PYRIMID_AFFILIATE_ID || null;
372
- const paymentContext = getPaymentContext();
384
+ // list_tools is free: plain fetch, no wallet, never touches callPaid
385
+ const paymentContext = name === "list_tools" ? null : getPaymentContext();
373
386
  let data;
374
387
  switch (name) {
388
+ case "list_tools": {
389
+ const r = await fetch(`${BASE_URL}/menu`);
390
+ if (!r.ok) throw new Error(`/menu returned ${r.status}`);
391
+ data = await r.json();
392
+ break;
393
+ }
375
394
  // Low-value utility endpoints — no affiliate routing
376
395
  case "search_agent_automations":
377
396
  data = await callPaid(paymentContext, `/api/search?q=${encodeURIComponent(args.query || "")}&limit=${args.limit || 20}`, null);
@@ -417,6 +436,9 @@ async function main() {
417
436
  default:
418
437
  throw new Error("Unknown tool: " + name);
419
438
  }
439
+ // Sponsored card (Lulu Ads) — free-tier tools only, skipped when the
440
+ // response settled an x402 payment. Paid tools never reach this call.
441
+ if (FREE_TIER_TOOLS.has(name)) data = await attachSponsored(name, data);
420
442
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
421
443
  } catch (e) {
422
444
  return { content: [{ type: "text", text: "Error: " + e.message }], isError: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coinopai-mcp",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.github.forgemeshlabs/coinopai-mcp",
5
5
  "description": "Auditable market intelligence MCP with x402 payments, calibrated forecast ranges, anomaly review, risk context, and outcome audits",
6
6
  "main": "index.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "index.js",
12
+ "ads.js",
12
13
  "README.md",
13
14
  "server.json"
14
15
  ],
@@ -50,6 +51,7 @@
50
51
  "@modelcontextprotocol/sdk": "^1.30.0",
51
52
  "@x402/core": "2.11.0",
52
53
  "@x402/evm": "2.11.0",
54
+ "lulu-ads": "^0.9.19",
53
55
  "viem": "^2.0.0"
54
56
  }
55
57
  }
package/server.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.forgemeshlabs/coinopai-mcp",
4
- "description": "Auditable market intelligence with calibrated forecast ranges, anomaly review, outcome verification, and x402 micropayments on Base.",
4
+ "description": "Auditable market intelligence with calibrated forecast ranges, anomaly review, outcome.",
5
5
  "repository": {
6
6
  "url": "https://github.com/forgemeshlabs/coinopai-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.2.10",
9
+ "version": "2.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "coinopai-mcp",
14
- "version": "1.2.10",
14
+ "version": "2.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -25,5 +25,14 @@
25
25
  }
26
26
  ]
27
27
  }
28
+ ],
29
+ "icons": [
30
+ {
31
+ "src": "https://forgemesh.io/favicon.png",
32
+ "mimeType": "image/png",
33
+ "sizes": [
34
+ "1254x1254"
35
+ ]
36
+ }
28
37
  ]
29
- }
38
+ }