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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.6",
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
- // Create invoice directly via LND REST API
278
- const res = await fetch(`${LND_HOST}/v1/invoices`, {
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
- value: amount_sats,
286
- memo: description + (agentName ? ` [${agentName}]` : ""),
287
- expiry: "3600",
318
+ amount_sats,
319
+ description,
288
320
  }),
289
321
  });
290
- const data = await res.json();
291
322
 
292
- if (res.ok && data.payment_request) {
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.payment_request,
299
- amount_sats: amount_sats,
300
- description: description,
301
- agent: agentName,
302
- payment_hash: data.r_hash,
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: `LND connection error: ${e.message}` }] };
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
- // Decode invoice first to get amount for policy check
351
- const decodeRes = await fetch(`${LND_HOST}/v1/payreq/${invoice}`, {
352
- headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
353
- });
354
- const decoded = await decodeRes.json();
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
- payment_request: invoice,
374
- fee_limit: { fixed: fee_limit_sats },
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: paidAmount,
394
- fee_sats: parseInt(data.fee || data.fee_sat || 0),
395
- agent: resolvedAgent?.name || null,
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 ${paidAmount} sats ⚡`,
381
+ message: `Paid ${data.amount_sats} sats via router (${data.rail || "lightning"}) ⚡`,
398
382
  }, null, 2),
399
383
  }],
400
384
  };
401
385
  }
402
- const errMsg = data.payment_error || data.message || JSON.stringify(data);
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: `LND connection error: ${e.message}` }] };
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 || !LND_HOST) {
433
+ if (!invoice) {
449
434
  return {
450
435
  content: [{
451
436
  type: "text",
452
437
  text: JSON.stringify({
453
438
  success: false,
454
- error: "Need LND node configured to pay invoice",
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: "Account suspended — routing verification failed. Contact support." }] };
449
+ return { content: [{ type: "text", text: "Account suspended — routing verification failed. Contact support." }] };
466
450
  }
467
451
 
468
- // Force routing through BK Block node as first hop
469
- const payRes = await fetch(`${LND_HOST}/v1/channels/transactions`, {
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
- payment_request: invoice,
477
- outgoing_chan_id: "", // LND will use available channels
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.payment_error) {
484
- return { content: [{ type: "text", text: `Lightning payment failed: ${payData.payment_error}` }] };
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
- return { content: [{ type: "text", text: "No preimage returned from payment" }] };
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
- message: `Paid ${amount} sats ⚡ for ${endpoint} API access`,
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
- if (!LND_HOST) {
704
- return { content: [{ type: "text", text: "Error: LND node not configured." }] };
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
- const payRes = await fetch(`${LND_HOST}/v1/channels/transactions`, {
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
- payment_request: invoiceData.pr,
771
- fee_limit: { fixed: Math.max(100, Math.floor(amount_sats * 0.01)) },
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 (payRes.ok && !payData.payment_error) {
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: parseInt(payData.fee || payData.fee_sat || 0),
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
- return { content: [{ type: "text", text: `Payment failed: ${payData.payment_error || JSON.stringify(payData)}` }] };
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
- if (!LND_HOST) {
812
- return { content: [{ type: "text", text: "Error: LND node not configured." }] };
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 res = await fetch(`${LND_HOST}/v1/payreq/${invoice}`, {
816
- headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
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 (!res.ok) {
821
- return { content: [{ type: "text", text: `Error decoding invoice: ${JSON.stringify(data)}` }] };
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: parseInt(data.num_satoshis || 0),
801
+ amount_sats: data.amount_sats,
835
802
  destination: data.destination,
836
803
  description: data.description || "",
837
804
  payment_hash: data.payment_hash,
838
- timestamp: new Date(timestamp * 1000).toISOString(),
839
- expires_at: expiresAt,
840
- is_expired: isExpired,
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: `LND error: ${e.message}` }] };
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);