agenticbtc-mcp 1.0.6 → 1.0.9
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/package.json +1 -1
- package/src/server.js +237 -164
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenticbtc-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Privacy-intelligent payments for AI agents — your privacy, your choice. Universal payment router with Lightning, Strike, Coinbase, PayPal, Venmo support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin",
|
package/src/server.js
CHANGED
|
@@ -7,12 +7,62 @@
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import crypto from "crypto";
|
|
10
11
|
|
|
11
12
|
const API_URL = process.env.AGENTICBTC_API_URL || process.env.AGENTBTC_API_URL || "http://localhost:8000";
|
|
12
13
|
const API_KEY = process.env.AGENTICBTC_API_KEY || process.env.AGENTBTC_API_KEY || "";
|
|
13
14
|
const LND_HOST = process.env.AGENTBTC_LND_HOST || "";
|
|
14
15
|
const LND_MACAROON = process.env.AGENTBTC_LND_MACAROON || "";
|
|
15
16
|
|
|
17
|
+
// X (Twitter) credentials — optional, enables social posting tools
|
|
18
|
+
const X_CONSUMER_KEY = process.env.TWITTER_CONSUMER_KEY || "";
|
|
19
|
+
const X_CONSUMER_SECRET = process.env.TWITTER_CONSUMER_SECRET || "";
|
|
20
|
+
const X_ACCESS_TOKEN = process.env.TWITTER_ACCESS_TOKEN || "";
|
|
21
|
+
const X_ACCESS_SECRET = process.env.TWITTER_ACCESS_SECRET || "";
|
|
22
|
+
const X_BEARER_TOKEN = process.env.TWITTER_BEARER_TOKEN || "";
|
|
23
|
+
const X_USER_ID = X_ACCESS_TOKEN ? X_ACCESS_TOKEN.split("-")[0] : "";
|
|
24
|
+
const X_ENABLED = !!(X_CONSUMER_KEY && X_ACCESS_TOKEN);
|
|
25
|
+
|
|
26
|
+
// OAuth 1.0a helper for X write operations
|
|
27
|
+
function buildXOAuthHeader(method, url) {
|
|
28
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
29
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
30
|
+
const oauthParams = {
|
|
31
|
+
oauth_consumer_key: X_CONSUMER_KEY,
|
|
32
|
+
oauth_nonce: nonce,
|
|
33
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
34
|
+
oauth_timestamp: timestamp,
|
|
35
|
+
oauth_token: X_ACCESS_TOKEN,
|
|
36
|
+
oauth_version: "1.0",
|
|
37
|
+
};
|
|
38
|
+
const sorted = Object.entries(oauthParams).sort(([a], [b]) => a.localeCompare(b));
|
|
39
|
+
const paramStr = sorted.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
40
|
+
const baseStr = `${method.toUpperCase()}&${encodeURIComponent(url)}&${encodeURIComponent(paramStr)}`;
|
|
41
|
+
const signingKey = `${encodeURIComponent(X_CONSUMER_SECRET)}&${encodeURIComponent(X_ACCESS_SECRET)}`;
|
|
42
|
+
const signature = crypto.createHmac("sha1", signingKey).update(baseStr).digest("base64");
|
|
43
|
+
return "OAuth " + Object.entries({ ...oauthParams, oauth_signature: signature })
|
|
44
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
45
|
+
.map(([k, v]) => `${encodeURIComponent(k)}="${encodeURIComponent(v)}"`)
|
|
46
|
+
.join(", ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function xPost(endpoint, body) {
|
|
50
|
+
const url = `https://api.x.com${endpoint}`;
|
|
51
|
+
const auth = buildXOAuthHeader("POST", url);
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { Authorization: auth, "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
return { status: res.status, data: await res.json() };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function xGet(endpoint) {
|
|
61
|
+
const url = `https://api.x.com${endpoint}`;
|
|
62
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${X_BEARER_TOKEN}` } });
|
|
63
|
+
return { status: res.status, data: await res.json() };
|
|
64
|
+
}
|
|
65
|
+
|
|
16
66
|
// BK Block routing node — all payments route through this node as first hop
|
|
17
67
|
// Routing fee: 0.5% (5000ppm) + 1 sat base
|
|
18
68
|
const BK_BLOCK_NODE_PUBKEY = process.env.BK_BLOCK_NODE_PUBKEY || "031aef3a70c08a6e2d96ba1c78eec66092723cdc41d546329df3f065b0f200bd3b";
|
|
@@ -259,55 +309,35 @@ server.tool(
|
|
|
259
309
|
agent: z.string().optional().describe("Agent wallet name or ID (optional)"),
|
|
260
310
|
},
|
|
261
311
|
async ({ amount_sats, description, agent }) => {
|
|
262
|
-
if (!LND_HOST) {
|
|
263
|
-
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Resolve agent if provided (for logging/tracking)
|
|
267
|
-
let agentName = null;
|
|
268
|
-
if (agent) {
|
|
269
|
-
const resolved = await resolveAgent(agent);
|
|
270
|
-
if (!resolved) {
|
|
271
|
-
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
|
|
272
|
-
}
|
|
273
|
-
agentName = resolved.name;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
312
|
try {
|
|
277
|
-
//
|
|
278
|
-
|
|
313
|
+
// Route through the AgenticBTC API so the invoice is stored in the DB
|
|
314
|
+
// and the settlement poller can credit the agent wallet on payment
|
|
315
|
+
const { status, data } = await apiCall("/api/v1/invoices", {
|
|
279
316
|
method: "POST",
|
|
280
|
-
headers: {
|
|
281
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
282
|
-
"Content-Type": "application/json",
|
|
283
|
-
},
|
|
284
317
|
body: JSON.stringify({
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
expiry: "3600",
|
|
318
|
+
amount_sats,
|
|
319
|
+
description,
|
|
288
320
|
}),
|
|
289
321
|
});
|
|
290
|
-
const data = await res.json();
|
|
291
322
|
|
|
292
|
-
if (
|
|
323
|
+
if (status === 200 && data.invoice) {
|
|
293
324
|
return {
|
|
294
325
|
content: [{
|
|
295
326
|
type: "text",
|
|
296
327
|
text: JSON.stringify({
|
|
297
328
|
success: true,
|
|
298
|
-
invoice: data.
|
|
299
|
-
amount_sats
|
|
300
|
-
description: description,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
expires: "1 hour",
|
|
329
|
+
invoice: data.invoice,
|
|
330
|
+
amount_sats,
|
|
331
|
+
description: data.description || description,
|
|
332
|
+
payment_hash: data.payment_hash,
|
|
333
|
+
expires: data.expires_in || "1 hour",
|
|
304
334
|
}, null, 2),
|
|
305
335
|
}],
|
|
306
336
|
};
|
|
307
337
|
}
|
|
308
|
-
return { content: [{ type: "text", text: `Error creating invoice: ${JSON.stringify(data)}` }] };
|
|
338
|
+
return { content: [{ type: "text", text: `Error creating invoice: ${data?.detail || JSON.stringify(data)}` }] };
|
|
309
339
|
} catch (e) {
|
|
310
|
-
return { content: [{ type: "text", text: `
|
|
340
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
311
341
|
}
|
|
312
342
|
}
|
|
313
343
|
);
|
|
@@ -322,87 +352,42 @@ server.tool(
|
|
|
322
352
|
fee_limit_sats: z.number().optional().default(100).describe("Max fee in sats"),
|
|
323
353
|
},
|
|
324
354
|
async ({ invoice, agent, fee_limit_sats }) => {
|
|
325
|
-
if (!LND_HOST) {
|
|
326
|
-
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Resolve agent for spending policy enforcement
|
|
330
|
-
let resolvedAgent = null;
|
|
331
|
-
if (agent) {
|
|
332
|
-
resolvedAgent = await resolveAgent(agent);
|
|
333
|
-
if (!resolvedAgent) {
|
|
334
|
-
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
|
|
335
|
-
}
|
|
336
|
-
if (!resolvedAgent.enabled) {
|
|
337
|
-
return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled — payments blocked` }] };
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Auto-detect agent from API key auth level
|
|
342
|
-
if (!resolvedAgent) {
|
|
343
|
-
const auth = await getAuthLevel();
|
|
344
|
-
if (auth.agent_id) {
|
|
345
|
-
resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
355
|
try {
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
const amountSats = parseInt(decoded.num_satoshis || 0);
|
|
356
|
-
|
|
357
|
-
// Check spending policy if agent is identified
|
|
358
|
-
if (resolvedAgent && amountSats > 0) {
|
|
359
|
-
const policy = await checkSpendingPolicy(resolvedAgent.id, amountSats);
|
|
360
|
-
if (!policy.allowed) {
|
|
361
|
-
return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Pay invoice directly via LND REST API
|
|
366
|
-
const res = await fetch(`${LND_HOST}/v1/channels/transactions`, {
|
|
356
|
+
// Route through the AgenticBTC payment router (POST /api/v1/payments).
|
|
357
|
+
// The router enforces spending policy, collects the 0.5% router fee,
|
|
358
|
+
// performs privacy-aware rail selection, and provides multi-rail failover.
|
|
359
|
+
// Do NOT call LND directly here.
|
|
360
|
+
const { status, data } = await apiCall("/api/v1/payments", {
|
|
367
361
|
method: "POST",
|
|
368
|
-
headers: {
|
|
369
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
370
|
-
"Content-Type": "application/json",
|
|
371
|
-
},
|
|
372
362
|
body: JSON.stringify({
|
|
373
|
-
|
|
374
|
-
|
|
363
|
+
invoice,
|
|
364
|
+
fee_limit_sats: fee_limit_sats ?? 100,
|
|
365
|
+
// Pass agent hint in metadata so the server can scope logging
|
|
366
|
+
...(agent ? { agent_hint: agent } : {}),
|
|
375
367
|
}),
|
|
376
368
|
});
|
|
377
|
-
const data = await res.json();
|
|
378
|
-
|
|
379
|
-
if (res.ok && !data.payment_error) {
|
|
380
|
-
const paidAmount = parseInt(data.value || data.value_sat || amountSats || 0);
|
|
381
|
-
|
|
382
|
-
// Log transaction against agent
|
|
383
|
-
if (resolvedAgent) {
|
|
384
|
-
await logTransaction(resolvedAgent.id, paidAmount, decoded.destination || "", decoded.description || "", "completed");
|
|
385
|
-
}
|
|
386
369
|
|
|
370
|
+
if (status === 200 && data.success) {
|
|
387
371
|
return {
|
|
388
372
|
content: [{
|
|
389
373
|
type: "text",
|
|
390
374
|
text: JSON.stringify({
|
|
391
375
|
success: true,
|
|
392
|
-
payment_hash: data.payment_hash,
|
|
393
|
-
amount_sats:
|
|
394
|
-
fee_sats:
|
|
395
|
-
|
|
376
|
+
payment_hash: data.payment_hash || data.payment_id,
|
|
377
|
+
amount_sats: data.amount_sats,
|
|
378
|
+
fee_sats: data.fee_sats,
|
|
379
|
+
rail: data.rail,
|
|
396
380
|
status: data.status || "SUCCEEDED",
|
|
397
|
-
message: `Paid ${
|
|
381
|
+
message: `Paid ${data.amount_sats} sats via router (${data.rail || "lightning"}) ⚡`,
|
|
398
382
|
}, null, 2),
|
|
399
383
|
}],
|
|
400
384
|
};
|
|
401
385
|
}
|
|
402
|
-
|
|
386
|
+
|
|
387
|
+
const errMsg = data?.detail || data?.error || data?.message || JSON.stringify(data);
|
|
403
388
|
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
404
389
|
} catch (e) {
|
|
405
|
-
return { content: [{ type: "text", text: `
|
|
390
|
+
return { content: [{ type: "text", text: `Router connection error: ${e.message}` }] };
|
|
406
391
|
}
|
|
407
392
|
}
|
|
408
393
|
);
|
|
@@ -445,14 +430,13 @@ server.tool(
|
|
|
445
430
|
const macaroon = data.macaroon;
|
|
446
431
|
const amount = data.amount || 0;
|
|
447
432
|
|
|
448
|
-
if (!invoice
|
|
433
|
+
if (!invoice) {
|
|
449
434
|
return {
|
|
450
435
|
content: [{
|
|
451
436
|
type: "text",
|
|
452
437
|
text: JSON.stringify({
|
|
453
438
|
success: false,
|
|
454
|
-
error: "
|
|
455
|
-
invoice,
|
|
439
|
+
error: "No invoice returned from L402 challenge — cannot pay",
|
|
456
440
|
amount,
|
|
457
441
|
}, null, 2),
|
|
458
442
|
}],
|
|
@@ -462,31 +446,29 @@ server.tool(
|
|
|
462
446
|
// Check routing status before paying
|
|
463
447
|
const routingStatus = await checkRoutingStatus();
|
|
464
448
|
if (routingStatus === "suspended") {
|
|
465
|
-
return { content: [{ type: "text", text: "
|
|
449
|
+
return { content: [{ type: "text", text: "Account suspended — routing verification failed. Contact support." }] };
|
|
466
450
|
}
|
|
467
451
|
|
|
468
|
-
//
|
|
469
|
-
|
|
452
|
+
// Pay the L402 invoice via the AgenticBTC payment router (NOT directly via LND).
|
|
453
|
+
// The router collects the 0.5% fee, applies privacy-aware rail selection, and
|
|
454
|
+
// provides multi-rail failover.
|
|
455
|
+
const { status: payStatus, data: payData } = await apiCall("/api/v1/payments", {
|
|
470
456
|
method: "POST",
|
|
471
|
-
headers: {
|
|
472
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
473
|
-
"Content-Type": "application/json",
|
|
474
|
-
},
|
|
475
457
|
body: JSON.stringify({
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
fee_limit: { fixed: 5000 }, // Allow routing fees
|
|
458
|
+
invoice,
|
|
459
|
+
fee_limit_sats: 5000,
|
|
479
460
|
}),
|
|
480
461
|
});
|
|
481
|
-
const payData = await payRes.json();
|
|
482
462
|
|
|
483
|
-
if (payData.
|
|
484
|
-
|
|
463
|
+
if (payStatus !== 200 || !payData.success) {
|
|
464
|
+
const errMsg = payData?.detail || payData?.error || payData?.payment_error || JSON.stringify(payData);
|
|
465
|
+
return { content: [{ type: "text", text: `Lightning payment failed: ${errMsg}` }] };
|
|
485
466
|
}
|
|
486
467
|
|
|
487
|
-
const preimage = payData.payment_preimage;
|
|
468
|
+
const preimage = payData.preimage || payData.payment_preimage;
|
|
488
469
|
if (!preimage) {
|
|
489
|
-
|
|
470
|
+
// Payment succeeded at router level but no preimage returned — report best-effort
|
|
471
|
+
return { content: [{ type: "text", text: "Payment succeeded but no preimage returned from router. L402 access may require preimage — check router logs." }] };
|
|
490
472
|
}
|
|
491
473
|
|
|
492
474
|
// Step 3: Access with L402 token
|
|
@@ -499,22 +481,16 @@ server.tool(
|
|
|
499
481
|
|
|
500
482
|
if (authRes.status === 200) {
|
|
501
483
|
const authData = await authRes.json();
|
|
502
|
-
// Report L402 payment for routing verification
|
|
503
|
-
await reportPayment(
|
|
504
|
-
payData.payment_hash || "",
|
|
505
|
-
amount,
|
|
506
|
-
"",
|
|
507
|
-
[BK_BLOCK_NODE_PUBKEY]
|
|
508
|
-
);
|
|
509
484
|
return {
|
|
510
485
|
content: [{
|
|
511
486
|
type: "text",
|
|
512
487
|
text: JSON.stringify({
|
|
513
488
|
success: true,
|
|
514
489
|
data: authData,
|
|
515
|
-
amount_paid_sats: amount,
|
|
490
|
+
amount_paid_sats: payData.amount_sats || amount,
|
|
516
491
|
payment_preimage: preimage,
|
|
517
|
-
|
|
492
|
+
rail: payData.rail,
|
|
493
|
+
message: `Paid ${payData.amount_sats || amount} sats via router for ${endpoint} API access`,
|
|
518
494
|
}, null, 2),
|
|
519
495
|
}],
|
|
520
496
|
};
|
|
@@ -700,9 +676,9 @@ server.tool(
|
|
|
700
676
|
comment: z.string().optional().default("").describe("Optional comment for the recipient"),
|
|
701
677
|
},
|
|
702
678
|
async ({ address, amount_sats, agent, comment }) => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
679
|
+
// No LND_HOST check needed — payment routes through the AgenticBTC REST API router.
|
|
680
|
+
// LNURL resolution (steps 1 & 2 below) is external HTTP; only step 3 (the payment)
|
|
681
|
+
// has changed to go through POST /api/v1/payments instead of directly to LND.
|
|
706
682
|
|
|
707
683
|
// Resolve agent for spending policy
|
|
708
684
|
let resolvedAgent = null;
|
|
@@ -759,25 +735,18 @@ server.tool(
|
|
|
759
735
|
return { content: [{ type: "text", text: `Error: No invoice returned from ${domain}` }] };
|
|
760
736
|
}
|
|
761
737
|
|
|
762
|
-
// Step 3: Pay the invoice via LND
|
|
763
|
-
|
|
738
|
+
// Step 3: Pay the invoice via the AgenticBTC payment router (NOT directly via LND).
|
|
739
|
+
// This ensures the 0.5% router fee is collected, privacy-aware rail selection is
|
|
740
|
+
// applied, and multi-rail failover is available.
|
|
741
|
+
const { status: payStatus, data: payData } = await apiCall("/api/v1/payments", {
|
|
764
742
|
method: "POST",
|
|
765
|
-
headers: {
|
|
766
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
767
|
-
"Content-Type": "application/json",
|
|
768
|
-
},
|
|
769
743
|
body: JSON.stringify({
|
|
770
|
-
|
|
771
|
-
|
|
744
|
+
invoice: invoiceData.pr,
|
|
745
|
+
fee_limit_sats: Math.max(100, Math.floor(amount_sats * 0.01)),
|
|
772
746
|
}),
|
|
773
747
|
});
|
|
774
|
-
const payData = await payRes.json();
|
|
775
748
|
|
|
776
|
-
if (
|
|
777
|
-
// Log transaction
|
|
778
|
-
if (resolvedAgent) {
|
|
779
|
-
await logTransaction(resolvedAgent.id, amount_sats, address, comment || `Lightning address payment to ${address}`, "completed");
|
|
780
|
-
}
|
|
749
|
+
if (payStatus === 200 && payData.success) {
|
|
781
750
|
return {
|
|
782
751
|
content: [{
|
|
783
752
|
type: "text",
|
|
@@ -785,15 +754,17 @@ server.tool(
|
|
|
785
754
|
success: true,
|
|
786
755
|
recipient: address,
|
|
787
756
|
amount_sats: amount_sats,
|
|
788
|
-
fee_sats:
|
|
757
|
+
fee_sats: payData.fee_sats || 0,
|
|
758
|
+
rail: payData.rail,
|
|
789
759
|
agent: resolvedAgent?.name || null,
|
|
790
|
-
payment_hash: payData.payment_hash,
|
|
791
|
-
message: `Sent ${amount_sats} sats to ${address} ⚡`,
|
|
760
|
+
payment_hash: payData.payment_hash || payData.payment_id,
|
|
761
|
+
message: `Sent ${amount_sats} sats to ${address} via router ⚡`,
|
|
792
762
|
}, null, 2),
|
|
793
763
|
}],
|
|
794
764
|
};
|
|
795
765
|
}
|
|
796
|
-
|
|
766
|
+
const errMsg = payData?.detail || payData?.error || payData?.payment_error || JSON.stringify(payData);
|
|
767
|
+
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
797
768
|
} catch (e) {
|
|
798
769
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
799
770
|
}
|
|
@@ -808,43 +779,37 @@ server.tool(
|
|
|
808
779
|
invoice: z.string().describe("BOLT11 Lightning invoice to decode"),
|
|
809
780
|
},
|
|
810
781
|
async ({ invoice }) => {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
782
|
+
// Route through the AgenticBTC REST API (POST /api/v1/decode) instead of calling
|
|
783
|
+
// LND directly, so the server-side LND credentials are used and no client-side
|
|
784
|
+
// LND config is required.
|
|
814
785
|
try {
|
|
815
|
-
const
|
|
816
|
-
|
|
786
|
+
const { status, data } = await apiCall("/api/v1/decode", {
|
|
787
|
+
method: "POST",
|
|
788
|
+
body: JSON.stringify({ invoice }),
|
|
817
789
|
});
|
|
818
|
-
const data = await res.json();
|
|
819
790
|
|
|
820
|
-
if (!
|
|
821
|
-
|
|
791
|
+
if (status !== 200 || !data.success) {
|
|
792
|
+
const errMsg = data?.detail || data?.error || JSON.stringify(data);
|
|
793
|
+
return { content: [{ type: "text", text: `Error decoding invoice: ${errMsg}` }] };
|
|
822
794
|
}
|
|
823
795
|
|
|
824
|
-
const expiry = parseInt(data.expiry || 3600);
|
|
825
|
-
const timestamp = parseInt(data.timestamp || 0);
|
|
826
|
-
const expiresAt = new Date((timestamp + expiry) * 1000).toISOString();
|
|
827
|
-
const isExpired = Date.now() > (timestamp + expiry) * 1000;
|
|
828
|
-
|
|
829
796
|
return {
|
|
830
797
|
content: [{
|
|
831
798
|
type: "text",
|
|
832
799
|
text: JSON.stringify({
|
|
833
800
|
success: true,
|
|
834
|
-
amount_sats:
|
|
801
|
+
amount_sats: data.amount_sats,
|
|
835
802
|
destination: data.destination,
|
|
836
803
|
description: data.description || "",
|
|
837
804
|
payment_hash: data.payment_hash,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
cltv_expiry: parseInt(data.cltv_expiry || 0),
|
|
842
|
-
num_route_hints: (data.route_hints || []).length,
|
|
805
|
+
expires_at: data.expires_at ? new Date(data.expires_at * 1000).toISOString() : null,
|
|
806
|
+
is_expired: data.is_expired,
|
|
807
|
+
cltv_expiry: data.cltv_expiry,
|
|
843
808
|
}, null, 2),
|
|
844
809
|
}],
|
|
845
810
|
};
|
|
846
811
|
} catch (e) {
|
|
847
|
-
return { content: [{ type: "text", text: `
|
|
812
|
+
return { content: [{ type: "text", text: `Router error: ${e.message}` }] };
|
|
848
813
|
}
|
|
849
814
|
}
|
|
850
815
|
);
|
|
@@ -1408,6 +1373,114 @@ server.tool(
|
|
|
1408
1373
|
}
|
|
1409
1374
|
);
|
|
1410
1375
|
|
|
1376
|
+
// ─── X (Twitter) Tools ────────────────────────────────────────────────────────
|
|
1377
|
+
|
|
1378
|
+
server.tool(
|
|
1379
|
+
"post_tweet",
|
|
1380
|
+
"Post a tweet from @agentbtcio. Use for announcing launches, updates, or responding to relevant discussions. Keep under 280 chars.",
|
|
1381
|
+
{
|
|
1382
|
+
text: z.string().max(280).describe("Tweet text (max 280 characters)"),
|
|
1383
|
+
reply_to_tweet_id: z.string().optional().describe("Tweet ID to reply to (optional)"),
|
|
1384
|
+
},
|
|
1385
|
+
async ({ text, reply_to_tweet_id }) => {
|
|
1386
|
+
if (!X_ENABLED) {
|
|
1387
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X credentials not configured. Set TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET in Claude Desktop config." }) }] };
|
|
1388
|
+
}
|
|
1389
|
+
const body = { text };
|
|
1390
|
+
if (reply_to_tweet_id) body.reply = { in_reply_to_tweet_id: reply_to_tweet_id };
|
|
1391
|
+
const { status, data } = await xPost("/2/tweets", body);
|
|
1392
|
+
if (status === 201) {
|
|
1393
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, tweet_id: data.data.id, url: `https://x.com/agentbtcio/status/${data.data.id}`, text }) }] };
|
|
1394
|
+
}
|
|
1395
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.detail || data.title || "Post failed", status }) }] };
|
|
1396
|
+
}
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
server.tool(
|
|
1400
|
+
"get_mentions",
|
|
1401
|
+
"Get recent @mentions and replies to @agentbtcio. Use to monitor engagement and find conversations to join.",
|
|
1402
|
+
{
|
|
1403
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of recent mentions to fetch (max 20)"),
|
|
1404
|
+
},
|
|
1405
|
+
async ({ limit }) => {
|
|
1406
|
+
if (!X_BEARER_TOKEN || !X_USER_ID) {
|
|
1407
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X bearer token or user ID not configured." }) }] };
|
|
1408
|
+
}
|
|
1409
|
+
const { status, data } = await xGet(
|
|
1410
|
+
`/2/users/${X_USER_ID}/mentions?max_results=${limit}&tweet.fields=created_at,author_id,conversation_id&expansions=author_id&user.fields=username`
|
|
1411
|
+
);
|
|
1412
|
+
if (status === 200) {
|
|
1413
|
+
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u.username]));
|
|
1414
|
+
const mentions = (data.data || []).map(t => ({
|
|
1415
|
+
id: t.id,
|
|
1416
|
+
text: t.text,
|
|
1417
|
+
from: "@" + (users[t.author_id] || t.author_id),
|
|
1418
|
+
created_at: t.created_at,
|
|
1419
|
+
url: `https://x.com/i/status/${t.id}`,
|
|
1420
|
+
}));
|
|
1421
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, count: mentions.length, mentions }) }] };
|
|
1422
|
+
}
|
|
1423
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Failed to fetch mentions", status }) }] };
|
|
1424
|
+
}
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
server.tool(
|
|
1428
|
+
"search_x",
|
|
1429
|
+
"Search recent X posts by keyword. Use to find conversations about AI agents, Lightning Network, Bitcoin payments, or AgenticBTC.",
|
|
1430
|
+
{
|
|
1431
|
+
query: z.string().describe("Search query (e.g. 'AI agents Lightning payments', 'agenticbtc', 'MCP tools bitcoin')"),
|
|
1432
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of results (max 20)"),
|
|
1433
|
+
},
|
|
1434
|
+
async ({ query, limit }) => {
|
|
1435
|
+
if (!X_BEARER_TOKEN) {
|
|
1436
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X bearer token not configured." }) }] };
|
|
1437
|
+
}
|
|
1438
|
+
const params = new URLSearchParams({ query: `${query} -is:retweet lang:en`, max_results: limit, "tweet.fields": "created_at,author_id", expansions: "author_id", "user.fields": "username" });
|
|
1439
|
+
const { status, data } = await xGet(`/2/tweets/search/recent?${params}`);
|
|
1440
|
+
if (status === 200) {
|
|
1441
|
+
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u.username]));
|
|
1442
|
+
const tweets = (data.data || []).map(t => ({
|
|
1443
|
+
id: t.id,
|
|
1444
|
+
text: t.text,
|
|
1445
|
+
from: "@" + (users[t.author_id] || t.author_id),
|
|
1446
|
+
created_at: t.created_at,
|
|
1447
|
+
url: `https://x.com/i/status/${t.id}`,
|
|
1448
|
+
}));
|
|
1449
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, query, count: tweets.length, tweets }) }] };
|
|
1450
|
+
}
|
|
1451
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Search failed", status }) }] };
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
server.tool(
|
|
1456
|
+
"get_own_timeline",
|
|
1457
|
+
"Get recent tweets posted by @agentbtcio. Use to review posting history before drafting new content.",
|
|
1458
|
+
{
|
|
1459
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of recent tweets to fetch"),
|
|
1460
|
+
},
|
|
1461
|
+
async ({ limit }) => {
|
|
1462
|
+
if (!X_BEARER_TOKEN || !X_USER_ID) {
|
|
1463
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X credentials not configured." }) }] };
|
|
1464
|
+
}
|
|
1465
|
+
const { status, data } = await xGet(
|
|
1466
|
+
`/2/users/${X_USER_ID}/tweets?max_results=${limit}&tweet.fields=created_at,public_metrics&exclude=retweets,replies`
|
|
1467
|
+
);
|
|
1468
|
+
if (status === 200) {
|
|
1469
|
+
const tweets = (data.data || []).map(t => ({
|
|
1470
|
+
id: t.id,
|
|
1471
|
+
text: t.text,
|
|
1472
|
+
created_at: t.created_at,
|
|
1473
|
+
likes: t.public_metrics?.like_count || 0,
|
|
1474
|
+
retweets: t.public_metrics?.retweet_count || 0,
|
|
1475
|
+
replies: t.public_metrics?.reply_count || 0,
|
|
1476
|
+
url: `https://x.com/agentbtcio/status/${t.id}`,
|
|
1477
|
+
}));
|
|
1478
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, count: tweets.length, tweets }) }] };
|
|
1479
|
+
}
|
|
1480
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Failed to fetch timeline", status }) }] };
|
|
1481
|
+
}
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1411
1484
|
// Start server
|
|
1412
1485
|
const transport = new StdioServerTransport();
|
|
1413
1486
|
await server.connect(transport);
|