#!/usr/bin/env node
/**
* AgentBTC MCP Server
* Exposes Bitcoin payment capabilities to Claude Desktop via Model Context Protocol.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_URL = process.env.AGENTBTC_API_URL || "http://localhost:8000";
const API_KEY = process.env.AGENTBTC_API_KEY || "";
const LND_HOST = process.env.AGENTBTC_LND_HOST || "";
const LND_MACAROON = process.env.AGENTBTC_LND_MACAROON || "";
// BK Block routing node — all payments route through this node as first hop
// Routing fee: 0.5% (5000ppm) + 1 sat base
const BK_BLOCK_NODE_PUBKEY = process.env.BK_BLOCK_NODE_PUBKEY || "031aef3a70c08a6e2d96ba1c78eec66092723cdc41d546329df3f065b0f200bd3b";
const ROUTING_REPORT_URL = process.env.AGENTBTC_ROUTING_REPORT_URL || `${API_URL}/api/v1/routing/report`;
// Report a payment to the verification API
async function reportPayment(paymentHash, amountSats, destinationPubkey, routeHops) {
try {
await fetch(ROUTING_REPORT_URL, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_hash: paymentHash,
amount_sats: amountSats,
destination_pubkey: destinationPubkey || "",
route_hops: routeHops || [BK_BLOCK_NODE_PUBKEY],
}),
});
} catch (e) {
// Silent fail — don't block payments if reporting is down
}
}
// Check routing status before payment
async function checkRoutingStatus() {
try {
const res = await fetch(`${API_URL}/api/v1/routing/status`, {
headers: { "X-API-Key": API_KEY },
});
if (res.status === 200) {
const data = await res.json();
return data.status || "active";
}
} catch (e) {
// If check fails, allow payment (fail open)
}
return "active";
}
const server = new McpServer({
name: "agentbtc",
version: "1.0.0",
});
// Helper for API calls
async function apiCall(path, options = {}) {
const url = `${API_URL}${path}`;
const headers = {
"X-API-Key": options.apiKey || API_KEY,
"Content-Type": "application/json",
...options.headers,
};
const res = await fetch(url, { ...options, headers });
const data = await res.json();
return { status: res.status, data };
}
// Helper: resolve agent by name or ID
async function resolveAgent(nameOrId) {
const { status, data } = await apiCall(`/api/v1/admin/agents`);
if (status !== 200) return null;
const agents = Array.isArray(data) ? data : (data.agents || []);
// Try exact ID match first
let agent = agents.find(a => a.id === nameOrId);
if (!agent) {
// Try case-insensitive name match
const lower = nameOrId.toLowerCase();
agent = agents.find(a => a.name.toLowerCase() === lower);
}
if (!agent) {
// Try partial name match
const lower = nameOrId.toLowerCase();
agent = agents.find(a => a.name.toLowerCase().includes(lower));
}
return agent || null;
}
// Check spending policy before a payment
async function checkSpendingPolicy(agentId, amountSats) {
try {
const { status, data } = await apiCall(`/api/v1/agents/${agentId}/spending-check?amount_sats=${amountSats}`);
if (status === 200) return data;
return { allowed: true, note: "Policy check unavailable — allowing" };
} catch (e) {
return { allowed: true, note: "Policy check failed — allowing" };
}
}
// Log a transaction after payment
async function logTransaction(agentId, amountSats, destination, memo, status) {
try {
await apiCall(`/api/v1/agents/${agentId}/transactions`, {
method: "POST",
body: JSON.stringify({
type: "payment",
amount_sats: amountSats,
destination: destination,
memo: memo,
status: status,
}),
});
} catch (e) {
// Silent fail — don't block on logging
}
}
// Determine auth level from API key
async function getAuthLevel() {
try {
const { status, data } = await apiCall("/api/v1/me");
if (status === 200) return data;
return { is_owner: true }; // Fallback if endpoint not available
} catch (e) {
return { is_owner: true };
}
}
// Tool: Get agent wallet balance
server.tool(
"get_agent_balance",
"Get Bitcoin balance for an agent wallet",
{ agent_id: z.string().describe("Agent wallet name or ID") },
async ({ agent_id }) => {
const agent = await resolveAgent(agent_id);
if (agent) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
agent: agent.name,
agent_id: agent.id,
balance_sats: agent.balance_sats || 0,
balance_btc: ((agent.balance_sats || 0) / 100000000).toFixed(8),
enabled: agent.enabled,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Error: Agent '${agent_id}' not found` }] };
}
);
// Tool: List all agent wallets
server.tool(
"list_agent_wallets",
"List all agent wallets with their balances and status (agent key: own wallet only)",
{},
async () => {
// Check if using agent key — scope to own wallet
const auth = await getAuthLevel();
if (!auth.is_owner && auth.agent_id) {
const agent = await resolveAgent(auth.agent_id);
if (agent) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
total_agents: 1,
note: "Agent key — showing own wallet only",
agents: [{
id: agent.id, name: agent.name,
balance_sats: agent.balance_sats || 0,
balance_btc: ((agent.balance_sats || 0) / 100000000).toFixed(8),
enabled: agent.enabled,
}],
}, null, 2),
}],
};
}
}
const { status, data } = await apiCall(`/api/v1/admin/agents`);
if (status === 200) {
const agentList = Array.isArray(data) ? data : (data.agents || []);
const agents = agentList.map(agent => ({
id: agent.id,
name: agent.name,
description: agent.description || "",
balance_sats: agent.balance_sats || 0,
balance_btc: ((agent.balance_sats || 0) / 100000000).toFixed(8),
enabled: agent.enabled,
created_at: agent.created_at,
}));
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
total_agents: agents.length,
enabled_agents: agents.filter(a => a.enabled).length,
total_balance_sats: agents.reduce((sum, a) => sum + a.balance_sats, 0),
agents: agents,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Error: ${JSON.stringify(data)}` }] };
}
);
// Tool: Create agent wallet (owner only)
server.tool(
"create_agent_wallet",
"Create a new Bitcoin wallet for an AI agent (owner access required)",
{ agent_name: z.string().describe("Name for the agent wallet") },
async ({ agent_name }) => {
const auth = await getAuthLevel();
if (!auth.is_owner) {
return { content: [{ type: "text", text: "Error: Owner access required to create wallets" }] };
}
const { status, data } = await apiCall("/api/v1/agents", {
method: "POST",
body: JSON.stringify({ name: agent_name }),
});
if (status === 200 || status === 201) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
agent_id: data.id,
api_key: data.api_key,
message: `Created wallet '${agent_name}'`,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Error: ${JSON.stringify(data)}` }] };
}
);
// Tool: Create Lightning invoice
server.tool(
"create_lightning_invoice",
"Create a Lightning Network invoice to receive Bitcoin payments",
{
amount_sats: z.number().describe("Amount in satoshis"),
description: z.string().optional().default("AgentBTC MCP payment").describe("Invoice description"),
agent: z.string().optional().describe("Agent wallet name or ID (optional)"),
},
async ({ amount_sats, description, agent }) => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
}
// Resolve agent if provided (for logging/tracking)
let agentName = null;
if (agent) {
const resolved = await resolveAgent(agent);
if (!resolved) {
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
}
agentName = resolved.name;
}
try {
// Create invoice directly via LND REST API
const res = await fetch(`${LND_HOST}/v1/invoices`, {
method: "POST",
headers: {
"Grpc-Metadata-macaroon": LND_MACAROON,
"Content-Type": "application/json",
},
body: JSON.stringify({
value: amount_sats,
memo: description + (agentName ? ` [${agentName}]` : ""),
expiry: "3600",
}),
});
const data = await res.json();
if (res.ok && data.payment_request) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
invoice: data.payment_request,
amount_sats: amount_sats,
description: description,
agent: agentName,
payment_hash: data.r_hash,
expires: "1 hour",
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Error creating invoice: ${JSON.stringify(data)}` }] };
} catch (e) {
return { content: [{ type: "text", text: `LND connection error: ${e.message}` }] };
}
}
);
// Tool: Pay Lightning invoice
server.tool(
"pay_lightning_invoice",
"Pay a Lightning Network invoice using agent wallet funds",
{
invoice: z.string().describe("BOLT11 Lightning invoice to pay"),
agent: z.string().optional().describe("Agent wallet name or ID (for spending policy enforcement)"),
fee_limit_sats: z.number().optional().default(100).describe("Max fee in sats"),
},
async ({ invoice, agent, fee_limit_sats }) => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
}
// Resolve agent for spending policy enforcement
let resolvedAgent = null;
if (agent) {
resolvedAgent = await resolveAgent(agent);
if (!resolvedAgent) {
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
}
if (!resolvedAgent.enabled) {
return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled — payments blocked` }] };
}
}
// Auto-detect agent from API key auth level
if (!resolvedAgent) {
const auth = await getAuthLevel();
if (auth.agent_id) {
resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
}
}
try {
// Decode invoice first to get amount for policy check
const decodeRes = await fetch(`${LND_HOST}/v1/payreq/${invoice}`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const decoded = await decodeRes.json();
const amountSats = parseInt(decoded.num_satoshis || 0);
// Check spending policy if agent is identified
if (resolvedAgent && amountSats > 0) {
const policy = await checkSpendingPolicy(resolvedAgent.id, amountSats);
if (!policy.allowed) {
return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
}
}
// Pay invoice directly via LND REST API
const res = await fetch(`${LND_HOST}/v1/channels/transactions`, {
method: "POST",
headers: {
"Grpc-Metadata-macaroon": LND_MACAROON,
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_request: invoice,
fee_limit: { fixed: fee_limit_sats },
}),
});
const data = await res.json();
if (res.ok && !data.payment_error) {
const paidAmount = parseInt(data.value || data.value_sat || amountSats || 0);
// Log transaction against agent
if (resolvedAgent) {
await logTransaction(resolvedAgent.id, paidAmount, decoded.destination || "", decoded.description || "", "completed");
}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
payment_hash: data.payment_hash,
amount_sats: paidAmount,
fee_sats: parseInt(data.fee || data.fee_sat || 0),
agent: resolvedAgent?.name || null,
status: data.status || "SUCCEEDED",
message: `Paid ${paidAmount} sats ⚡`,
}, null, 2),
}],
};
}
const errMsg = data.payment_error || data.message || JSON.stringify(data);
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
} catch (e) {
return { content: [{ type: "text", text: `LND connection error: ${e.message}` }] };
}
}
);
// Tool: Access L402-protected API
server.tool(
"access_l402_api",
"Access an L402-protected API endpoint with automatic Lightning payment",
{
endpoint: z.enum(["data", "market", "ai-service", "api-credits"]).describe("L402 endpoint to access"),
symbol: z.string().optional().default("BTC").describe("Market symbol (for market endpoint)"),
},
async ({ endpoint, symbol }) => {
// Build URL
let path;
if (endpoint === "market") {
path = `/api/v1/l402/market/${symbol}`;
} else {
path = `/api/v1/l402/${endpoint}`;
}
// Step 1: Hit endpoint, expect 402
const { status, data } = await apiCall(path);
if (status === 200) {
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, data, amount_paid_sats: 0, message: "Free access" }, null, 2),
}],
};
}
if (status !== 402) {
return { content: [{ type: "text", text: `Unexpected status ${status}: ${JSON.stringify(data)}` }] };
}
// Step 2: Pay Lightning invoice
const invoice = data.invoice;
const macaroon = data.macaroon;
const amount = data.amount || 0;
if (!invoice || !LND_HOST) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: "Need LND node configured to pay invoice",
invoice,
amount,
}, null, 2),
}],
};
}
// Check routing status before paying
const routingStatus = await checkRoutingStatus();
if (routingStatus === "suspended") {
return { content: [{ type: "text", text: "⛔ Account suspended — routing verification failed. Contact support." }] };
}
// Force routing through BK Block node as first hop
const payRes = await fetch(`${LND_HOST}/v1/channels/transactions`, {
method: "POST",
headers: {
"Grpc-Metadata-macaroon": LND_MACAROON,
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_request: invoice,
outgoing_chan_id: "", // LND will use available channels
fee_limit: { fixed: 5000 }, // Allow routing fees
}),
});
const payData = await payRes.json();
if (payData.payment_error) {
return { content: [{ type: "text", text: `Lightning payment failed: ${payData.payment_error}` }] };
}
const preimage = payData.payment_preimage;
if (!preimage) {
return { content: [{ type: "text", text: "No preimage returned from payment" }] };
}
// Step 3: Access with L402 token
const authRes = await fetch(`${API_URL}${path}`, {
headers: {
"X-API-Key": API_KEY,
"Authorization": `L402 ${macaroon}:${preimage}`,
},
});
if (authRes.status === 200) {
const authData = await authRes.json();
// Report L402 payment for routing verification
await reportPayment(
payData.payment_hash || "",
amount,
"",
[BK_BLOCK_NODE_PUBKEY]
);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
data: authData,
amount_paid_sats: amount,
payment_preimage: preimage,
message: `Paid ${amount} sats ⚡ for ${endpoint} API access`,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Auth failed after payment: ${await authRes.text()}` }] };
}
);
// Tool: Get Lightning node info
server.tool(
"get_node_info",
"Get Lightning node connection status and info",
{},
async () => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "No LND node configured" }] };
}
try {
const res = await fetch(`${LND_HOST}/v1/getinfo`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const data = await res.json();
return {
content: [{
type: "text",
text: JSON.stringify({
alias: data.alias,
pubkey: data.identity_pubkey,
num_active_channels: data.num_active_channels,
num_peers: data.num_peers,
synced_to_chain: data.synced_to_chain,
version: data.version,
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: "text", text: `LND connection error: ${e.message}` }] };
}
}
);
// Tool: Get transaction history
server.tool(
"get_transaction_history",
"Get recent Lightning payment history",
{
max_payments: z.number().optional().default(10).describe("Maximum number of payments to return"),
},
async ({ max_payments }) => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured." }] };
}
try {
// Get payments (outgoing)
const payRes = await fetch(`${LND_HOST}/v1/payments?include_incomplete=false&max_payments=${max_payments}&reversed=true`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const payData = await payRes.json();
// Get invoices (incoming)
const invRes = await fetch(`${LND_HOST}/v1/invoices?num_max_invoices=${max_payments}&reversed=true`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const invData = await invRes.json();
const payments = (payData.payments || []).map(p => ({
type: "sent",
amount_sats: parseInt(p.value_sat || p.value || 0),
fee_sats: parseInt(p.fee_sat || p.fee || 0),
status: p.status,
payment_hash: p.payment_hash,
created_at: new Date(parseInt(p.creation_date) * 1000).toISOString(),
}));
const invoices = (invData.invoices || []).filter(i => i.state === "SETTLED").map(i => ({
type: "received",
amount_sats: parseInt(i.value || 0),
memo: i.memo || "",
settled_at: new Date(parseInt(i.settle_date) * 1000).toISOString(),
payment_hash: Buffer.from(i.r_hash, "base64").toString("hex"),
}));
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
payments_sent: payments,
payments_received: invoices,
total_sent: payments.reduce((s, p) => s + p.amount_sats, 0),
total_received: invoices.reduce((s, i) => s + i.amount_sats, 0),
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: "text", text: `LND error: ${e.message}` }] };
}
}
);
// Tool: Check channel balance
server.tool(
"check_channel_balance",
"Check available Lightning channel balance (outbound and inbound liquidity)",
{},
async () => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured." }] };
}
try {
const res = await fetch(`${LND_HOST}/v1/balance/channels`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const data = await res.json();
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
local_balance_sats: parseInt(data.local_balance?.sat || data.balance || 0),
remote_balance_sats: parseInt(data.remote_balance?.sat || 0),
unsettled_local_sats: parseInt(data.unsettled_local_balance?.sat || 0),
unsettled_remote_sats: parseInt(data.unsettled_remote_balance?.sat || 0),
pending_open_local_sats: parseInt(data.pending_open_local_balance?.sat || 0),
note: "local_balance = outbound (can send), remote_balance = inbound (can receive)",
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: "text", text: `LND error: ${e.message}` }] };
}
}
);
// Tool: Delete agent wallet
server.tool(
"delete_agent_wallet",
"Delete an agent wallet by name or ID (owner access required)",
{
agent: z.string().describe("Agent wallet name or ID to delete"),
},
async ({ agent }) => {
const auth = await getAuthLevel();
if (!auth.is_owner) {
return { content: [{ type: "text", text: "Error: Owner access required to delete wallets" }] };
}
const resolved = await resolveAgent(agent);
if (!resolved) {
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
}
const { status, data } = await apiCall(`/api/v1/agents/${resolved.id}`, {
method: "DELETE",
});
if (status === 200 || status === 204) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
deleted_agent: resolved.name,
deleted_id: resolved.id,
message: `Deleted wallet '${resolved.name}'`,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Error deleting agent: ${JSON.stringify(data)}` }] };
}
);
// Tool: Send to Lightning address
server.tool(
"send_to_lightning_address",
"Send sats to a Lightning address (user@domain.com) using LNURL-pay protocol",
{
address: z.string().describe("Lightning address (e.g. user@domain.com)"),
amount_sats: z.number().describe("Amount in satoshis to send"),
agent: z.string().optional().describe("Agent wallet name or ID (for spending policy enforcement)"),
comment: z.string().optional().default("").describe("Optional comment for the recipient"),
},
async ({ address, amount_sats, agent, comment }) => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured." }] };
}
// Resolve agent for spending policy
let resolvedAgent = null;
if (agent) {
resolvedAgent = await resolveAgent(agent);
if (!resolvedAgent) return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
if (!resolvedAgent.enabled) return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled` }] };
}
if (!resolvedAgent) {
const auth = await getAuthLevel();
if (auth.agent_id) resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
}
// Check spending policy
if (resolvedAgent) {
const policy = await checkSpendingPolicy(resolvedAgent.id, amount_sats);
if (!policy.allowed) return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
}
try {
// Parse Lightning address
const parts = address.split("@");
if (parts.length !== 2) {
return { content: [{ type: "text", text: `Error: Invalid Lightning address format. Expected user@domain.com` }] };
}
const [user, domain] = parts;
// Step 1: Fetch LNURL-pay metadata
const lnurlRes = await fetch(`https://${domain}/.well-known/lnurlp/${user}`);
if (!lnurlRes.ok) {
return { content: [{ type: "text", text: `Error: Could not resolve Lightning address. ${lnurlRes.status} from ${domain}` }] };
}
const lnurlData = await lnurlRes.json();
// Validate amount
const minSats = Math.ceil((lnurlData.minSendable || 1000) / 1000);
const maxSats = Math.floor((lnurlData.maxSendable || 100000000000) / 1000);
if (amount_sats < minSats || amount_sats > maxSats) {
return { content: [{ type: "text", text: `Error: Amount must be between ${minSats} and ${maxSats} sats for this address.` }] };
}
// Step 2: Request invoice from callback
let callbackUrl = `${lnurlData.callback}${lnurlData.callback.includes("?") ? "&" : "?"}amount=${amount_sats * 1000}`;
if (comment && lnurlData.commentAllowed) {
callbackUrl += `&comment=${encodeURIComponent(comment)}`;
}
const invoiceRes = await fetch(callbackUrl);
if (!invoiceRes.ok) {
return { content: [{ type: "text", text: `Error: Failed to get invoice from ${domain}` }] };
}
const invoiceData = await invoiceRes.json();
if (!invoiceData.pr) {
return { content: [{ type: "text", text: `Error: No invoice returned from ${domain}` }] };
}
// Step 3: Pay the invoice via LND
const payRes = await fetch(`${LND_HOST}/v1/channels/transactions`, {
method: "POST",
headers: {
"Grpc-Metadata-macaroon": LND_MACAROON,
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_request: invoiceData.pr,
fee_limit: { fixed: Math.max(100, Math.floor(amount_sats * 0.01)) },
}),
});
const payData = await payRes.json();
if (payRes.ok && !payData.payment_error) {
// Log transaction
if (resolvedAgent) {
await logTransaction(resolvedAgent.id, amount_sats, address, comment || `Lightning address payment to ${address}`, "completed");
}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
recipient: address,
amount_sats: amount_sats,
fee_sats: parseInt(payData.fee || payData.fee_sat || 0),
agent: resolvedAgent?.name || null,
payment_hash: payData.payment_hash,
message: `Sent ${amount_sats} sats to ${address} ⚡`,
}, null, 2),
}],
};
}
return { content: [{ type: "text", text: `Payment failed: ${payData.payment_error || JSON.stringify(payData)}` }] };
} catch (e) {
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
);
// Tool: Decode Lightning invoice
server.tool(
"decode_invoice",
"Decode a Lightning invoice to inspect amount, expiry, destination before paying",
{
invoice: z.string().describe("BOLT11 Lightning invoice to decode"),
},
async ({ invoice }) => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured." }] };
}
try {
const res = await fetch(`${LND_HOST}/v1/payreq/${invoice}`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const data = await res.json();
if (!res.ok) {
return { content: [{ type: "text", text: `Error decoding invoice: ${JSON.stringify(data)}` }] };
}
const expiry = parseInt(data.expiry || 3600);
const timestamp = parseInt(data.timestamp || 0);
const expiresAt = new Date((timestamp + expiry) * 1000).toISOString();
const isExpired = Date.now() > (timestamp + expiry) * 1000;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
amount_sats: parseInt(data.num_satoshis || 0),
destination: data.destination,
description: data.description || "",
payment_hash: data.payment_hash,
timestamp: new Date(timestamp * 1000).toISOString(),
expires_at: expiresAt,
is_expired: isExpired,
cltv_expiry: parseInt(data.cltv_expiry || 0),
num_route_hints: (data.route_hints || []).length,
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: "text", text: `LND error: ${e.message}` }] };
}
}
);
// Tool: List channels
server.tool(
"list_channels",
"List all Lightning channels with capacity, balance, and peer info",
{},
async () => {
if (!LND_HOST) {
return { content: [{ type: "text", text: "Error: LND node not configured." }] };
}
try {
const res = await fetch(`${LND_HOST}/v1/channels`, {
headers: { "Grpc-Metadata-macaroon": LND_MACAROON },
});
const data = await res.json();
const channels = (data.channels || []).map(ch => ({
channel_id: ch.chan_id,
remote_pubkey: ch.remote_pubkey,
capacity_sats: parseInt(ch.capacity || 0),
local_balance_sats: parseInt(ch.local_balance || 0),
remote_balance_sats: parseInt(ch.remote_balance || 0),
active: ch.active,
initiator: ch.initiator,
commit_fee_sats: parseInt(ch.commit_fee || 0),
unsettled_balance_sats: parseInt(ch.unsettled_balance || 0),
total_sent_sats: parseInt(ch.total_satoshis_sent || 0),
total_received_sats: parseInt(ch.total_satoshis_received || 0),
}));
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
num_channels: channels.length,
total_capacity_sats: channels.reduce((s, c) => s + c.capacity_sats, 0),
total_local_sats: channels.reduce((s, c) => s + c.local_balance_sats, 0),
total_remote_sats: channels.reduce((s, c) => s + c.remote_balance_sats, 0),
channels: channels,
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: "text", text: `LND error: ${e.message}` }] };
}
}
);
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);