Skip to main content
Glama
StevenGeller

LDK MCP Server

by StevenGeller

ldk_close_channel

Close Lightning Network channels cooperatively or force close them when needed, managing channel lifecycle in LDK-based iOS Lightning wallets.

Instructions

Close a Lightning channel cooperatively or force close

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
channelIdYesChannel ID to close
forceNoForce close the channel

Implementation Reference

  • src/index.ts:38-62 (registration)
    Registration of the ldk_close_channel tool (imported as closeChannelTool) in the main tools array used by the MCP server
    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,
    ];
  • The main handler function for the ldk_close_channel tool. Calls LightningService.closeChannel and formats response with Swift iOS example code.
      execute: async (args: any): Promise<ToolResult> => {
        try {
          const success = await lightningService.closeChannel(args.channelId);
    
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                success,
                channelId: args.channelId,
                closeType: args.force ? 'force' : 'cooperative',
                message: 'Channel closing initiated',
                swiftExample: `
    // Swift code for channel closing in your iOS app
    import LightningDevKit
    
    class ChannelCloser {
        let channelManager: ChannelManager
        
        func closeChannel(
            channelId: String,
            counterpartyNodeId: String,
            force: Bool = false
        ) throws {
            guard let channelIdData = Data(hexString: channelId),
                  channelIdData.count == 32,
                  let nodeIdData = Data(hexString: counterpartyNodeId),
                  nodeIdData.count == 33 else {
                throw ChannelError.invalidParameters
            }
            
            let result: Result_NoneAPIErrorZ
            
            if force {
                // Force close the channel
                result = channelManager.forceCloseBroadcastingLatestTxn(
                    channelId: channelIdData.bytes,
                    counterpartyNodeId: nodeIdData.bytes
                )
            } else {
                // Cooperative close
                result = channelManager.closeChannel(
                    channelId: channelIdData.bytes,
                    counterpartyNodeId: nodeIdData.bytes
                )
            }
            
            guard result.isOk() else {
                throw ChannelError.closeFailed(result.getError()!)
            }
        }
        
        // Handle spendable outputs after channel close
        func handleSpendableOutputs(event: Event.SpendableOutputs) async throws {
            let outputs = event.getOutputs()
            
            // Get destination address from wallet
            let address = try wallet.getAddress(addressIndex: .new)
            let script = try Address(
                address: address.address.asString(),
                network: .testnet
            ).scriptPubkey().toBytes()
            
            // Create sweep transaction
            let result = keysManager.spendSpendableOutputs(
                descriptors: outputs,
                outputs: [], // No additional outputs
                changeDestinationScript: script,
                feerateSatPer1000Weight: 1000,
                locktime: nil // Current block height recommended
            )
            
            guard result.isOk(), let tx = result.getValue() else {
                throw ChannelError.sweepFailed
            }
            
            // Broadcast sweep transaction
            try await broadcastTransaction(Data(tx))
        }
    }
    
    // SwiftUI view for channel management
    struct ChannelDetailView: View {
        let channel: ChannelDetails
        @State private var showingCloseAlert = false
        @State private var isClosing = false
        @State private var closeError: String?
        @Environment(\\.dismiss) private var dismiss
        
        var body: some View {
            ScrollView {
                VStack(spacing: 20) {
                    // Channel status card
                    VStack(alignment: .leading, spacing: 12) {
                        HStack {
                            Circle()
                                .fill(channel.isUsable ? Color.green : Color.orange)
                                .frame(width: 12, height: 12)
                            
                            Text(channelStatus)
                                .font(.headline)
                            
                            Spacer()
                            
                            if let scid = channel.getShortChannelId() {
                                Text(scid.description)
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }
                        }
                        
                        Divider()
                        
                        // Balances
                        VStack(spacing: 8) {
                            BalanceRow(
                                label: "Local Balance",
                                amount: localBalanceSats,
                                color: .green
                            )
                            
                            BalanceRow(
                                label: "Remote Balance",
                                amount: remoteBalanceSats,
                                color: .blue
                            )
                            
                            BalanceRow(
                                label: "Channel Capacity",
                                amount: Int(channel.channelValueSatoshis),
                                color: .primary
                            )
                        }
                    }
                    .padding()
                    .background(Color(UIColor.secondarySystemBackground))
                    .cornerRadius(12)
                    
                    // Actions
                    VStack(spacing: 12) {
                        Button(action: { showingCloseAlert = true }) {
                            Label("Close Channel", systemImage: "xmark.circle")
                                .foregroundColor(.red)
                                .frame(maxWidth: .infinity)
                        }
                        .buttonStyle(.bordered)
                        .disabled(isClosing)
                    }
                }
                .padding()
            }
            .navigationTitle("Channel Details")
            .navigationBarTitleDisplayMode(.inline)
            .alert("Close Channel", isPresented: $showingCloseAlert) {
                Button("Cooperative Close", role: .destructive) {
                    closeChannel(force: false)
                }
                Button("Force Close", role: .destructive) {
                    closeChannel(force: true)
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Cooperative close is cheaper but requires the peer to be online. Force close can be done immediately but costs more in fees.")
            }
            .alert("Error", isPresented: .constant(closeError != nil)) {
                Button("OK") { closeError = nil }
            } message: {
                Text(closeError ?? "")
            }
            .overlay {
                if isClosing {
                    Color.black.opacity(0.3)
                        .ignoresSafeArea()
                    
                    ProgressView("Closing channel...")
                        .padding()
                        .background(Color(UIColor.systemBackground))
                        .cornerRadius(10)
                        .shadow(radius: 5)
                }
            }
        }
        
        var channelStatus: String {
            if channel.isUsable {
                return "Active"
            } else if channel.isChannelReady {
                return "Ready"
            } else {
                return "Pending"
            }
        }
        
        var localBalanceSats: Int {
            Int(channel.balanceMsat / 1000)
        }
        
        var remoteBalanceSats: Int {
            Int(channel.channelValueSatoshis) - localBalanceSats
        }
        
        func closeChannel(force: Bool) {
            Task {
                isClosing = true
                defer { isClosing = false }
                
                do {
                    try LDKManager.shared.closeChannel(
                        channelId: channel.getChannelId().toHex(),
                        counterpartyNodeId: channel.getCounterpartyNodeId().toHex(),
                        force: force
                    )
                    
                    dismiss()
                } catch {
                    closeError = error.localizedDescription
                }
            }
        }
    }
    
    struct BalanceRow: View {
        let label: String
        let amount: Int
        let color: Color
        
        var body: some View {
            HStack {
                Text(label)
                    .foregroundColor(.secondary)
                Spacer()
                Text("\\(amount.formatted()) sats")
                    .fontWeight(.medium)
                    .foregroundColor(color)
            }
        }
    }`.trim()
              }, 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 parameters for the ldk_close_channel tool: channelId (required) and optional force flag.
    inputSchema: {
      type: 'object',
      properties: {
        channelId: {
          type: 'string',
          description: 'Channel ID to close'
        },
        force: {
          type: 'boolean',
          description: 'Force close the channel',
          default: false
        }
      },
      required: ['channelId']
    },
  • Helper method in LightningService that implements the channel closing logic (mocked simulation for demo purposes). Called by the tool handler.
    async closeChannel(channelId: string): Promise<boolean> {
      const channel = this.channels.get(channelId);
      if (!channel) {
        throw new Error('Channel not found');
      }
    
      channel.state = ChannelState.Closing;
      channel.isUsable = false;
      this.nodeInfo.numUsableChannels--;
      
      // Simulate async closing
      setTimeout(() => {
        channel.state = ChannelState.Closed;
        this.channels.delete(channelId);
        this.nodeInfo.numChannels--;
      }, 5000);
    
      return true;
    }

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