Skip to main content
Glama
StevenGeller

LDK MCP Server

by StevenGeller

ldk_network_graph

Perform Lightning network graph operations including RapidGossipSync, route queries, and node/channel information retrieval for LDK-based wallet development.

Instructions

Get network graph operations and RapidGossipSync implementation

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
operationYesNetwork graph operation to perform
nodeIdNoNode ID for node_info operation (optional)
channelIdNoChannel ID for channel_info operation (optional)

Implementation Reference

  • The main handler function for the 'ldk_network_graph' tool. It selects and returns comprehensive Swift code examples for LDK NetworkGraph and RapidGossipSync operations (setup, sync, route finding, node/channel queries, update handling) based on the input 'operation' parameter.
      execute: async (args: any): Promise<ToolResult> => {
        const swiftExamples: Record<string, string> = {
          setup: `
    // Network Graph and RapidGossipSync setup in Swift
    import LightningDevKit
    
    class NetworkGraphManager {
        private let networkGraph: Bindings.NetworkGraph
        private let rapidGossipSync: Bindings.RapidGossipSync
        private let logger: Bindings.Logger
        private let network: Bindings.Network
        private var lastSyncTimestamp: UInt64 = 0
        
        init(network: Bindings.Network, logger: Bindings.Logger) {
            self.network = network
            self.logger = logger
            
            // Initialize or load network graph
            if let savedGraph = loadNetworkGraphFromDisk() {
                let readResult = Bindings.NetworkGraph.read(ser: savedGraph, arg: logger)
                if readResult.isOk() {
                    self.networkGraph = readResult.getValue()!
                } else {
                    self.networkGraph = Bindings.NetworkGraph(network: network, logger: logger)
                }
            } else {
                self.networkGraph = Bindings.NetworkGraph(network: network, logger: logger)
            }
            
            // Initialize RapidGossipSync
            self.rapidGossipSync = Bindings.RapidGossipSync(networkGraph: networkGraph, logger: logger)
            
            // Load last sync timestamp
            self.lastSyncTimestamp = UserDefaults.standard.object(forKey: "lastGossipSync") as? UInt64 ?? 0
        }
        
        // Persist network graph to disk
        func saveNetworkGraph() throws {
            let serialized = networkGraph.write()
            let documentsPath = FileManager.default.urls(
                for: .documentDirectory,
                in: .userDomainMask
            ).first!
            let graphPath = documentsPath.appendingPathComponent("network_graph.bin")
            
            try Data(serialized).write(to: graphPath)
        }
        
        private func loadNetworkGraphFromDisk() -> [UInt8]? {
            let documentsPath = FileManager.default.urls(
                for: .documentDirectory,
                in: .userDomainMask
            ).first!
            let graphPath = documentsPath.appendingPathComponent("network_graph.bin")
            
            guard let data = try? Data(contentsOf: graphPath) else {
                return nil
            }
            
            return [UInt8](data)
        }
    }`.trim(),
    
          rapid_gossip_sync: `
    // RapidGossipSync implementation for efficient network updates
    import LightningDevKit
    
    extension NetworkGraphManager {
        // Sync network graph using RapidGossipSync
        func syncNetworkGraph() async throws {
            let currentTime = UInt64(Date().timeIntervalSince1970)
            
            // Build RGS URL with last sync timestamp
            let baseUrl = network == .bitcoin ? 
                "https://rapidsync.lightningdevkit.org/snapshot" :
                "https://rapidsync.lightningdevkit.org/testnet/snapshot"
            
            let url = URL(string: "\\(baseUrl)/\\(lastSyncTimestamp)")!
            
            // Download snapshot
            let (data, response) = try await URLSession.shared.data(from: url)
            
            guard let httpResponse = response as? HTTPURLResponse,
                  httpResponse.statusCode == 200 else {
                throw NetworkError.syncFailed
            }
            
            // Apply update to network graph
            let result = rapidGossipSync.updateNetworkGraphNoStd(
                updateData: [UInt8](data),
                currentTimeUnix: currentTime
            )
            
            if result.isOk() {
                // Update last sync timestamp
                lastSyncTimestamp = currentTime
                UserDefaults.standard.set(lastSyncTimestamp, forKey: "lastGossipSync")
                
                // Persist updated graph
                try saveNetworkGraph()
                
                print("RapidGossipSync successful - updated to timestamp: \\(currentTime)")
            } else if let error = result.getError() {
                throw NetworkError.graphUpdateFailed(error.getValueAsDecodeError()?.getDescription() ?? "Unknown error")
            }
        }
        
        // Schedule automatic sync
        func startAutomaticSync(interval: TimeInterval = 3600) {
            Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
                Task {
                    do {
                        try await self.syncNetworkGraph()
                    } catch {
                        print("Automatic sync failed: \\(error)")
                    }
                }
            }
            
            // Perform initial sync
            Task {
                try? await syncNetworkGraph()
            }
        }
        
        // Manual incremental update
        func applyGossipUpdate(_ update: [UInt8]) -> Bool {
            let currentTime = UInt64(Date().timeIntervalSince1970)
            let result = rapidGossipSync.updateNetworkGraphNoStd(
                updateData: update,
                currentTimeUnix: currentTime
            )
            
            return result.isOk()
        }
    }
    
    // Background task for iOS
    extension NetworkGraphManager {
        func performBackgroundSync() async -> Bool {
            do {
                try await syncNetworkGraph()
                return true
            } catch {
                print("Background sync failed: \\(error)")
                return false
            }
        }
    }`.trim(),
    
          query_routes: `
    // Query routes using the network graph
    import LightningDevKit
    
    extension NetworkGraphManager {
        // Find routes for a payment
        func findRoute(
            to destination: [UInt8],
            amountMsat: UInt64,
            paymentParams: Bindings.PaymentParameters
        ) -> Result<[RouteInfo], RoutingError> {
            let channelManager = LDKManager.shared.channelManager
            let ourNodeId = channelManager.getOurNodeId()
            let channels = channelManager.listUsableChannels()
            
            // Create route parameters
            let routeParams = Bindings.RouteParameters(
                paymentParamsArg: paymentParams,
                finalValueMsatArg: amountMsat,
                maxTotalRoutingFeeMsatArg: amountMsat / 100 // 1% max fee
            )
            
            // Use router to find route
            let router = LDKManager.shared.router
            let inflightHtlcs = Bindings.InFlightHtlcs()
            
            let routeResult = router.findRoute(
                payer: ourNodeId,
                routeParams: routeParams,
                firstHops: channels,
                inflightHtlcs: inflightHtlcs
            )
            
            if routeResult.isOk(), let route = routeResult.getValue() {
                return .success(analyzeRoute(route))
            } else if let error = routeResult.getError() {
                return .failure(.routingFailed(error.getDescription()))
            }
            
            return .failure(.noRoute)
        }
        
        // Analyze route details
        private func analyzeRoute(_ route: Bindings.Route) -> [RouteInfo] {
            return route.getPaths().map { path in
                let hops = path.getHops().map { hop in
                    HopInfo(
                        pubkey: Data(hop.getPubkey()).hexString,
                        shortChannelId: hop.getShortChannelId(),
                        feeMsat: hop.getFeeMsat(),
                        cltvExpiryDelta: hop.getCltvExpiryDelta()
                    )
                }
                
                let totalFees = path.getFeeMsat()
                let finalValueMsat = path.getFinalValueMsat()
                
                return RouteInfo(
                    hops: hops,
                    totalFeeMsat: totalFees,
                    totalAmountMsat: finalValueMsat
                )
            }
        }
    }
    
    struct RouteInfo {
        let hops: [HopInfo]
        let totalFeeMsat: UInt64
        let totalAmountMsat: UInt64
    }
    
    struct HopInfo {
        let pubkey: String
        let shortChannelId: UInt64
        let feeMsat: UInt64
        let cltvExpiryDelta: UInt32
    }
    
    enum RoutingError: Error {
        case noRoute
        case routingFailed(String)
        case insufficientBalance
    }`.trim(),
    
          node_info: `
    // Query node information from network graph
    import LightningDevKit
    
    extension NetworkGraphManager {
        // Get information about a specific node
        func getNodeInfo(nodeId: String) -> NodeDetails? {
            guard let pubkeyData = Data(hexString: nodeId),
                  pubkeyData.count == 33 else {
                return nil
            }
            
            let nodeIdObj = Bindings.NodeId(pubkey: pubkeyData.bytes)
            
            guard let nodeInfo = networkGraph.readOnly().node(nodeId: nodeIdObj) else {
                return nil
            }
            
            // Extract node details
            let channels = nodeInfo.getChannels()
            let announcement = nodeInfo.getAnnouncementMessage()
            
            var nodeDetails = NodeDetails(
                nodeId: nodeId,
                alias: "",
                color: "",
                addresses: [],
                features: [],
                channelCount: channels.count,
                totalCapacityMsat: 0
            )
            
            // Parse announcement if available
            if let announcement = announcement {
                let contents = announcement.getContents()
                nodeDetails.alias = String(data: Data(contents.getAlias()), encoding: .utf8) ?? ""
                nodeDetails.color = Data(contents.getRgb()).hexString
                
                // Parse addresses
                nodeDetails.addresses = contents.getAddresses().compactMap { address in
                    parseNetworkAddress(address)
                }
                
                // Features
                if let features = contents.getFeatures() {
                    nodeDetails.features = parseNodeFeatures(features)
                }
            }
            
            // Calculate total capacity
            for channelId in channels {
                if let channelInfo = networkGraph.readOnly().channel(shortChannelId: channelId) {
                    nodeDetails.totalCapacityMsat += channelInfo.getCapacityMsat() ?? 0
                }
            }
            
            return nodeDetails
        }
        
        // Get all known nodes
        func getAllNodes() -> [NodeSummary] {
            let readOnly = networkGraph.readOnly()
            let nodeIds = readOnly.listNodes()
            
            return nodeIds.compactMap { nodeId in
                guard let nodeInfo = readOnly.node(nodeId: nodeId) else {
                    return nil
                }
                
                let pubkey = Data(nodeId.asSlice()).hexString
                let channels = nodeInfo.getChannels()
                
                var alias = ""
                if let announcement = nodeInfo.getAnnouncementMessage() {
                    alias = String(data: Data(announcement.getContents().getAlias()), encoding: .utf8) ?? ""
                }
                
                return NodeSummary(
                    nodeId: pubkey,
                    alias: alias,
                    channelCount: channels.count,
                    isReachable: !channels.isEmpty
                )
            }
        }
        
        private func parseNetworkAddress(_ address: Bindings.SocketAddress) -> String? {
            switch address.getValueType() {
            case .TcpIpV4:
                if let ipv4 = address.getValueAsTcpIpV4() {
                    let addr = ipv4.getAddr()
                    return "\\(addr.0).\\(addr.1).\\(addr.2).\\(addr.3):\\(ipv4.getPort())"
                }
            case .TcpIpV6:
                if let ipv6 = address.getValueAsTcpIpV6() {
                    return "[\\(ipv6.getAddr().map{String($0)}.joined(separator:":"))]:\\(ipv6.getPort())"
                }
            case .OnionV3:
                if let onion = address.getValueAsOnionV3() {
                    return "\\(Data(onion.getEd25519Pubkey()).hexString).onion:\\(onion.getPort())"
                }
            default:
                break
            }
            return nil
        }
        
        private func parseNodeFeatures(_ features: Bindings.NodeFeatures) -> [String] {
            var featureList: [String] = []
            
            if features.supportsVariableLengthOnion() {
                featureList.append("Variable Length Onion")
            }
            if features.supportsStaticRemoteKey() {
                featureList.append("Static Remote Key")
            }
            if features.supportsAnchors() {
                featureList.append("Anchor Outputs")
            }
            
            return featureList
        }
    }
    
    struct NodeDetails {
        var nodeId: String
        var alias: String
        var color: String
        var addresses: [String]
        var features: [String]
        var channelCount: Int
        var totalCapacityMsat: UInt64
    }
    
    struct NodeSummary {
        let nodeId: String
        let alias: String
        let channelCount: Int
        let isReachable: Bool
    }`.trim(),
    
          channel_info: `
    // Query channel information from network graph
    import LightningDevKit
    
    extension NetworkGraphManager {
        // Get information about a specific channel
        func getChannelInfo(shortChannelId: UInt64) -> ChannelDetails? {
            guard let channelInfo = networkGraph.readOnly().channel(shortChannelId: shortChannelId) else {
                return nil
            }
            
            let node1 = Data(channelInfo.getNodeOne().asSlice()).hexString
            let node2 = Data(channelInfo.getNodeTwo().asSlice()).hexString
            
            var details = ChannelDetails(
                shortChannelId: shortChannelId,
                node1: node1,
                node2: node2,
                capacityMsat: channelInfo.getCapacityMsat(),
                node1Policy: nil,
                node2Policy: nil,
                lastUpdate: nil
            )
            
            // Get directional policies
            if let node1Policy = channelInfo.getOneToTwo() {
                details.node1Policy = parseChannelPolicy(node1Policy)
            }
            
            if let node2Policy = channelInfo.getTwoToOne() {
                details.node2Policy = parseChannelPolicy(node2Policy)
            }
            
            // Get announcement details
            if let announcement = channelInfo.getAnnouncementMessage() {
                let contents = announcement.getContents()
                details.lastUpdate = Date(timeIntervalSince1970: TimeInterval(contents.getTimestamp()))
            }
            
            return details
        }
        
        // Get all channels for a node
        func getNodeChannels(nodeId: String) -> [ChannelSummary] {
            guard let pubkeyData = Data(hexString: nodeId),
                  pubkeyData.count == 33 else {
                return []
            }
            
            let nodeIdObj = Bindings.NodeId(pubkey: pubkeyData.bytes)
            
            guard let nodeInfo = networkGraph.readOnly().node(nodeId: nodeIdObj) else {
                return []
            }
            
            return nodeInfo.getChannels().compactMap { channelId in
                guard let channelInfo = networkGraph.readOnly().channel(shortChannelId: channelId) else {
                    return nil
                }
                
                let node1 = Data(channelInfo.getNodeOne().asSlice()).hexString
                let node2 = Data(channelInfo.getNodeTwo().asSlice()).hexString
                let isNode1 = node1 == nodeId
                let remoteNode = isNode1 ? node2 : node1
                
                // Get our policy
                let ourPolicy = isNode1 ? channelInfo.getOneToTwo() : channelInfo.getTwoToOne()
                let theirPolicy = isNode1 ? channelInfo.getTwoToOne() : channelInfo.getOneToTwo()
                
                return ChannelSummary(
                    shortChannelId: channelId,
                    remoteNode: remoteNode,
                    capacityMsat: channelInfo.getCapacityMsat(),
                    isEnabled: ourPolicy?.getEnabled() ?? false,
                    baseFee: ourPolicy?.getFeeBaseMsat() ?? 0,
                    feeRate: ourPolicy?.getFeeProportionalMillionths() ?? 0,
                    remoteBaseFee: theirPolicy?.getFeeBaseMsat() ?? 0,
                    remoteFeeRate: theirPolicy?.getFeeProportionalMillionths() ?? 0
                )
            }
        }
        
        private func parseChannelPolicy(_ update: Bindings.ChannelUpdateInfo) -> ChannelPolicy {
            return ChannelPolicy(
                enabled: update.getEnabled(),
                cltvExpiryDelta: update.getCltvExpiryDelta(),
                htlcMinimumMsat: update.getHtlcMinimumMsat(),
                htlcMaximumMsat: update.getHtlcMaximumMsat(),
                feeBaseMsat: update.getFeeBaseMsat(),
                feeProportionalMillionths: update.getFeeProportionalMillionths(),
                lastUpdate: Date(timeIntervalSince1970: TimeInterval(update.getLastUpdate()))
            )
        }
    }
    
    struct ChannelDetails {
        let shortChannelId: UInt64
        let node1: String
        let node2: String
        let capacityMsat: UInt64?
        var node1Policy: ChannelPolicy?
        var node2Policy: ChannelPolicy?
        var lastUpdate: Date?
    }
    
    struct ChannelPolicy {
        let enabled: Bool
        let cltvExpiryDelta: UInt16
        let htlcMinimumMsat: UInt64
        let htlcMaximumMsat: UInt64?
        let feeBaseMsat: UInt32
        let feeProportionalMillionths: UInt32
        let lastUpdate: Date
    }
    
    struct ChannelSummary {
        let shortChannelId: UInt64
        let remoteNode: String
        let capacityMsat: UInt64?
        let isEnabled: Bool
        let baseFee: UInt32
        let feeRate: UInt32
        let remoteBaseFee: UInt32
        let remoteFeeRate: UInt32
    }`.trim(),
    
          update_handling: `
    // Handle network graph updates
    import LightningDevKit
    
    extension NetworkGraphManager {
        // Process channel announcement
        func handleChannelAnnouncement(_ announcement: Bindings.ChannelAnnouncement) -> Bool {
            let result = networkGraph.updateChannelFromAnnouncement(
                msg: announcement,
                utxoLookup: nil
            )
            
            if result.isOk() {
                // Persist updated graph
                try? saveNetworkGraph()
                return true
            }
            
            return false
        }
        
        // Process channel update
        func handleChannelUpdate(_ update: Bindings.ChannelUpdate) -> Bool {
            let result = networkGraph.updateChannelFromUnsignedAnnouncement(
                msg: update.getContents(),
                utxoLookup: nil
            )
            
            if result.isOk() {
                try? saveNetworkGraph()
                return true
            }
            
            return false
        }
        
        // Process node announcement
        func handleNodeAnnouncement(_ announcement: Bindings.NodeAnnouncement) -> Bool {
            let result = networkGraph.updateNodeFromAnnouncement(msg: announcement)
            
            if result.isOk() {
                try? saveNetworkGraph()
                return true
            }
            
            return false
        }
        
        // Prune old channels
        func pruneStaleChannels() {
            let currentTime = UInt64(Date().timeIntervalSince1970)
            let twoWeeksAgo = currentTime - (14 * 24 * 60 * 60)
            
            networkGraph.removeStaleChannelsAndTrackedNodes(currentTimeUnix: twoWeeksAgo)
            
            // Persist pruned graph
            try? saveNetworkGraph()
        }
        
        // Monitor graph statistics
        func getGraphStatistics() -> GraphStatistics {
            let readOnly = networkGraph.readOnly()
            let nodeCount = readOnly.listNodes().count
            
            var channelCount = 0
            var totalCapacityMsat: UInt64 = 0
            
            for node in readOnly.listNodes() {
                if let nodeInfo = readOnly.node(nodeId: node) {
                    let channels = nodeInfo.getChannels()
                    channelCount += channels.count
                    
                    for channelId in channels {
                        if let channelInfo = readOnly.channel(shortChannelId: channelId) {
                            totalCapacityMsat += channelInfo.getCapacityMsat() ?? 0
                        }
                    }
                }
            }
            
            // Channels are counted twice (once per node)
            channelCount /= 2
            
            return GraphStatistics(
                nodeCount: nodeCount,
                channelCount: channelCount,
                totalCapacityMsat: totalCapacityMsat,
                lastSync: Date(timeIntervalSince1970: TimeInterval(lastSyncTimestamp))
            )
        }
    }
    
    struct GraphStatistics {
        let nodeCount: Int
        let channelCount: Int
        let totalCapacityMsat: UInt64
        let lastSync: Date
    }
    
    // SwiftUI View for Network Graph Stats
    struct NetworkGraphView: View {
        @StateObject private var viewModel = NetworkGraphViewModel()
        
        var body: some View {
            List {
                Section("Network Statistics") {
                    StatRow(label: "Nodes", value: "\\(viewModel.stats.nodeCount)")
                    StatRow(label: "Channels", value: "\\(viewModel.stats.channelCount)")
                    StatRow(label: "Total Capacity", value: "\\(viewModel.stats.totalCapacityMsat / 1_000_000_000) BTC")
                }
                
                Section("Sync Status") {
                    HStack {
                        Text("Last Sync")
                        Spacer()
                        Text(viewModel.stats.lastSync, style: .relative)
                            .foregroundColor(.secondary)
                    }
                    
                    Button("Sync Now") {
                        Task {
                            await viewModel.syncNetworkGraph()
                        }
                    }
                    .disabled(viewModel.isSyncing)
                }
                
                if viewModel.isSyncing {
                    Section {
                        HStack {
                            ProgressView()
                            Text("Syncing network graph...")
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Network Graph")
            .onAppear {
                viewModel.loadStatistics()
            }
        }
    }
    
    struct StatRow: View {
        let label: String
        let value: String
        
        var body: some View {
            HStack {
                Text(label)
                Spacer()
                Text(value)
                    .fontWeight(.medium)
                    .foregroundColor(.secondary)
            }
        }
    }`.trim()
        };
    
        try {
          const code = swiftExamples[args.operation];
          if (!code) {
            throw new Error(`Unknown operation: ${args.operation}`);
          }
    
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                success: true,
                operation: args.operation,
                swiftCode: code,
                description: `NetworkGraph and RapidGossipSync implementation for ${args.operation}`
              }, null, 2)
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                success: false,
                error: error instanceof Error ? error.message : 'Unknown error'
              }, null, 2)
            }],
            isError: true
          };
        }
      }
  • Input schema defining the parameters for the tool, including required 'operation' enum and optional nodeId/channelId.
    inputSchema: {
      type: 'object',
      properties: {
        operation: {
          type: 'string',
          enum: [
            'setup',
            'rapid_gossip_sync',
            'query_routes',
            'node_info',
            'channel_info',
            'update_handling'
          ],
          description: 'Network graph operation to perform'
        },
        nodeId: {
          type: 'string',
          description: 'Node ID for node_info operation (optional)'
        },
        channelId: {
          type: 'string',
          description: 'Channel ID for channel_info operation (optional)'
        }
      },
      required: ['operation']
    },
  • src/index.ts:38-62 (registration)
    The networkGraphTool is imported and included in the central 'tools' array used by the MCP server to register and expose all available tools.
    const tools = [
      generateInvoiceTool,
      payInvoiceTool,
      getChannelStatusTool,
      getNodeInfoTool,
      backupStateTool,
      keychainTestTool,
      backgroundTestTool,
      pushNotificationTool,
      biometricAuthTool,
      createChannelTool,
      closeChannelTool,
      getBalanceTool,
      decodeInvoiceTool,
      listPaymentsTool,
      estimateFeeTool,
      generateMnemonicTool,
      deriveAddressTool,
      getSwiftCodeTool,
      getArchitectureTool,
      testScenarioTool,
      networkGraphTool,
      eventHandlingTool,
      chainSyncTool,
    ];

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/StevenGeller/ldk-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server