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;
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions the two close modes but doesn't explain critical behaviors like permission requirements, irreversible effects, timing implications, or error conditions. For a destructive operation like channel closure, this lack of detail is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that states the core functionality without unnecessary words. It's appropriately sized and front-loaded with the essential action, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive operation with no annotations and no output schema, the description is incomplete. It doesn't cover critical context like what happens after closure (funds recovery, timing), error handling, or return values. Given the complexity of Lightning channel operations, more behavioral detail is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both parameters ('channelId' and 'force'). The description adds minimal value by implying the 'force' parameter enables force closing, but doesn't provide additional context like consequences of force vs cooperative closure. Baseline 3 is appropriate when schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('close') and resource ('Lightning channel'), distinguishing it from sibling tools like 'ldk_create_channel' (creation) and 'ldk_channel_status' (status check). It also specifies the two operational modes (cooperative vs force close), making the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives, such as when to choose cooperative vs force close, or prerequisites like channel state. It mentions both modes but doesn't explain their implications or trade-offs, leaving usage decisions unclear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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