agenticbtc-mcp 1.0.7 → 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 +224 -131
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";
|
|
@@ -302,87 +352,42 @@ server.tool(
|
|
|
302
352
|
fee_limit_sats: z.number().optional().default(100).describe("Max fee in sats"),
|
|
303
353
|
},
|
|
304
354
|
async ({ invoice, agent, fee_limit_sats }) => {
|
|
305
|
-
if (!LND_HOST) {
|
|
306
|
-
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Resolve agent for spending policy enforcement
|
|
310
|
-
let resolvedAgent = null;
|
|
311
|
-
if (agent) {
|
|
312
|
-
resolvedAgent = await resolveAgent(agent);
|
|
313
|
-
if (!resolvedAgent) {
|
|
314
|
-
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
|
|
315
|
-
}
|
|
316
|
-
if (!resolvedAgent.enabled) {
|
|
317
|
-
return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled — payments blocked` }] };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Auto-detect agent from API key auth level
|
|
322
|
-
if (!resolvedAgent) {
|
|
323
|
-
const auth = await getAuthLevel();
|
|
324
|
-
if (auth.agent_id) {
|
|
325
|
-
resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
355
|
try {
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
const amountSats = parseInt(decoded.num_satoshis || 0);
|
|
336
|
-
|
|
337
|
-
// Check spending policy if agent is identified
|
|
338
|
-
if (resolvedAgent && amountSats > 0) {
|
|
339
|
-
const policy = await checkSpendingPolicy(resolvedAgent.id, amountSats);
|
|
340
|
-
if (!policy.allowed) {
|
|
341
|
-
return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Pay invoice directly via LND REST API
|
|
346
|
-
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", {
|
|
347
361
|
method: "POST",
|
|
348
|
-
headers: {
|
|
349
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
350
|
-
"Content-Type": "application/json",
|
|
351
|
-
},
|
|
352
362
|
body: JSON.stringify({
|
|
353
|
-
|
|
354
|
-
|
|
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 } : {}),
|
|
355
367
|
}),
|
|
356
368
|
});
|
|
357
|
-
const data = await res.json();
|
|
358
|
-
|
|
359
|
-
if (res.ok && !data.payment_error) {
|
|
360
|
-
const paidAmount = parseInt(data.value || data.value_sat || amountSats || 0);
|
|
361
|
-
|
|
362
|
-
// Log transaction against agent
|
|
363
|
-
if (resolvedAgent) {
|
|
364
|
-
await logTransaction(resolvedAgent.id, paidAmount, decoded.destination || "", decoded.description || "", "completed");
|
|
365
|
-
}
|
|
366
369
|
|
|
370
|
+
if (status === 200 && data.success) {
|
|
367
371
|
return {
|
|
368
372
|
content: [{
|
|
369
373
|
type: "text",
|
|
370
374
|
text: JSON.stringify({
|
|
371
375
|
success: true,
|
|
372
|
-
payment_hash: data.payment_hash,
|
|
373
|
-
amount_sats:
|
|
374
|
-
fee_sats:
|
|
375
|
-
|
|
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,
|
|
376
380
|
status: data.status || "SUCCEEDED",
|
|
377
|
-
message: `Paid ${
|
|
381
|
+
message: `Paid ${data.amount_sats} sats via router (${data.rail || "lightning"}) ⚡`,
|
|
378
382
|
}, null, 2),
|
|
379
383
|
}],
|
|
380
384
|
};
|
|
381
385
|
}
|
|
382
|
-
|
|
386
|
+
|
|
387
|
+
const errMsg = data?.detail || data?.error || data?.message || JSON.stringify(data);
|
|
383
388
|
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
384
389
|
} catch (e) {
|
|
385
|
-
return { content: [{ type: "text", text: `
|
|
390
|
+
return { content: [{ type: "text", text: `Router connection error: ${e.message}` }] };
|
|
386
391
|
}
|
|
387
392
|
}
|
|
388
393
|
);
|
|
@@ -425,14 +430,13 @@ server.tool(
|
|
|
425
430
|
const macaroon = data.macaroon;
|
|
426
431
|
const amount = data.amount || 0;
|
|
427
432
|
|
|
428
|
-
if (!invoice
|
|
433
|
+
if (!invoice) {
|
|
429
434
|
return {
|
|
430
435
|
content: [{
|
|
431
436
|
type: "text",
|
|
432
437
|
text: JSON.stringify({
|
|
433
438
|
success: false,
|
|
434
|
-
error: "
|
|
435
|
-
invoice,
|
|
439
|
+
error: "No invoice returned from L402 challenge — cannot pay",
|
|
436
440
|
amount,
|
|
437
441
|
}, null, 2),
|
|
438
442
|
}],
|
|
@@ -442,31 +446,29 @@ server.tool(
|
|
|
442
446
|
// Check routing status before paying
|
|
443
447
|
const routingStatus = await checkRoutingStatus();
|
|
444
448
|
if (routingStatus === "suspended") {
|
|
445
|
-
return { content: [{ type: "text", text: "
|
|
449
|
+
return { content: [{ type: "text", text: "Account suspended — routing verification failed. Contact support." }] };
|
|
446
450
|
}
|
|
447
451
|
|
|
448
|
-
//
|
|
449
|
-
|
|
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", {
|
|
450
456
|
method: "POST",
|
|
451
|
-
headers: {
|
|
452
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
453
|
-
"Content-Type": "application/json",
|
|
454
|
-
},
|
|
455
457
|
body: JSON.stringify({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
fee_limit: { fixed: 5000 }, // Allow routing fees
|
|
458
|
+
invoice,
|
|
459
|
+
fee_limit_sats: 5000,
|
|
459
460
|
}),
|
|
460
461
|
});
|
|
461
|
-
const payData = await payRes.json();
|
|
462
462
|
|
|
463
|
-
if (payData.
|
|
464
|
-
|
|
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}` }] };
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
const preimage = payData.payment_preimage;
|
|
468
|
+
const preimage = payData.preimage || payData.payment_preimage;
|
|
468
469
|
if (!preimage) {
|
|
469
|
-
|
|
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." }] };
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
// Step 3: Access with L402 token
|
|
@@ -479,22 +481,16 @@ server.tool(
|
|
|
479
481
|
|
|
480
482
|
if (authRes.status === 200) {
|
|
481
483
|
const authData = await authRes.json();
|
|
482
|
-
// Report L402 payment for routing verification
|
|
483
|
-
await reportPayment(
|
|
484
|
-
payData.payment_hash || "",
|
|
485
|
-
amount,
|
|
486
|
-
"",
|
|
487
|
-
[BK_BLOCK_NODE_PUBKEY]
|
|
488
|
-
);
|
|
489
484
|
return {
|
|
490
485
|
content: [{
|
|
491
486
|
type: "text",
|
|
492
487
|
text: JSON.stringify({
|
|
493
488
|
success: true,
|
|
494
489
|
data: authData,
|
|
495
|
-
amount_paid_sats: amount,
|
|
490
|
+
amount_paid_sats: payData.amount_sats || amount,
|
|
496
491
|
payment_preimage: preimage,
|
|
497
|
-
|
|
492
|
+
rail: payData.rail,
|
|
493
|
+
message: `Paid ${payData.amount_sats || amount} sats via router for ${endpoint} API access`,
|
|
498
494
|
}, null, 2),
|
|
499
495
|
}],
|
|
500
496
|
};
|
|
@@ -680,9 +676,9 @@ server.tool(
|
|
|
680
676
|
comment: z.string().optional().default("").describe("Optional comment for the recipient"),
|
|
681
677
|
},
|
|
682
678
|
async ({ address, amount_sats, agent, comment }) => {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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.
|
|
686
682
|
|
|
687
683
|
// Resolve agent for spending policy
|
|
688
684
|
let resolvedAgent = null;
|
|
@@ -739,25 +735,18 @@ server.tool(
|
|
|
739
735
|
return { content: [{ type: "text", text: `Error: No invoice returned from ${domain}` }] };
|
|
740
736
|
}
|
|
741
737
|
|
|
742
|
-
// Step 3: Pay the invoice via LND
|
|
743
|
-
|
|
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", {
|
|
744
742
|
method: "POST",
|
|
745
|
-
headers: {
|
|
746
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
747
|
-
"Content-Type": "application/json",
|
|
748
|
-
},
|
|
749
743
|
body: JSON.stringify({
|
|
750
|
-
|
|
751
|
-
|
|
744
|
+
invoice: invoiceData.pr,
|
|
745
|
+
fee_limit_sats: Math.max(100, Math.floor(amount_sats * 0.01)),
|
|
752
746
|
}),
|
|
753
747
|
});
|
|
754
|
-
const payData = await payRes.json();
|
|
755
748
|
|
|
756
|
-
if (
|
|
757
|
-
// Log transaction
|
|
758
|
-
if (resolvedAgent) {
|
|
759
|
-
await logTransaction(resolvedAgent.id, amount_sats, address, comment || `Lightning address payment to ${address}`, "completed");
|
|
760
|
-
}
|
|
749
|
+
if (payStatus === 200 && payData.success) {
|
|
761
750
|
return {
|
|
762
751
|
content: [{
|
|
763
752
|
type: "text",
|
|
@@ -765,15 +754,17 @@ server.tool(
|
|
|
765
754
|
success: true,
|
|
766
755
|
recipient: address,
|
|
767
756
|
amount_sats: amount_sats,
|
|
768
|
-
fee_sats:
|
|
757
|
+
fee_sats: payData.fee_sats || 0,
|
|
758
|
+
rail: payData.rail,
|
|
769
759
|
agent: resolvedAgent?.name || null,
|
|
770
|
-
payment_hash: payData.payment_hash,
|
|
771
|
-
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 ⚡`,
|
|
772
762
|
}, null, 2),
|
|
773
763
|
}],
|
|
774
764
|
};
|
|
775
765
|
}
|
|
776
|
-
|
|
766
|
+
const errMsg = payData?.detail || payData?.error || payData?.payment_error || JSON.stringify(payData);
|
|
767
|
+
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
777
768
|
} catch (e) {
|
|
778
769
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
779
770
|
}
|
|
@@ -788,43 +779,37 @@ server.tool(
|
|
|
788
779
|
invoice: z.string().describe("BOLT11 Lightning invoice to decode"),
|
|
789
780
|
},
|
|
790
781
|
async ({ invoice }) => {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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.
|
|
794
785
|
try {
|
|
795
|
-
const
|
|
796
|
-
|
|
786
|
+
const { status, data } = await apiCall("/api/v1/decode", {
|
|
787
|
+
method: "POST",
|
|
788
|
+
body: JSON.stringify({ invoice }),
|
|
797
789
|
});
|
|
798
|
-
const data = await res.json();
|
|
799
790
|
|
|
800
|
-
if (!
|
|
801
|
-
|
|
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}` }] };
|
|
802
794
|
}
|
|
803
795
|
|
|
804
|
-
const expiry = parseInt(data.expiry || 3600);
|
|
805
|
-
const timestamp = parseInt(data.timestamp || 0);
|
|
806
|
-
const expiresAt = new Date((timestamp + expiry) * 1000).toISOString();
|
|
807
|
-
const isExpired = Date.now() > (timestamp + expiry) * 1000;
|
|
808
|
-
|
|
809
796
|
return {
|
|
810
797
|
content: [{
|
|
811
798
|
type: "text",
|
|
812
799
|
text: JSON.stringify({
|
|
813
800
|
success: true,
|
|
814
|
-
amount_sats:
|
|
801
|
+
amount_sats: data.amount_sats,
|
|
815
802
|
destination: data.destination,
|
|
816
803
|
description: data.description || "",
|
|
817
804
|
payment_hash: data.payment_hash,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
cltv_expiry: parseInt(data.cltv_expiry || 0),
|
|
822
|
-
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,
|
|
823
808
|
}, null, 2),
|
|
824
809
|
}],
|
|
825
810
|
};
|
|
826
811
|
} catch (e) {
|
|
827
|
-
return { content: [{ type: "text", text: `
|
|
812
|
+
return { content: [{ type: "text", text: `Router error: ${e.message}` }] };
|
|
828
813
|
}
|
|
829
814
|
}
|
|
830
815
|
);
|
|
@@ -1388,6 +1373,114 @@ server.tool(
|
|
|
1388
1373
|
}
|
|
1389
1374
|
);
|
|
1390
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
|
+
|
|
1391
1484
|
// Start server
|
|
1392
1485
|
const transport = new StdioServerTransport();
|
|
1393
1486
|
await server.connect(transport);
|