Skip to main content
Glama
StevenGeller

LDK MCP Server

by StevenGeller

ldk_test_scenario

Execute Lightning Network development test scenarios to validate payment flows, channel operations, and edge cases for iOS wallet development.

Instructions

Run complete Lightning development scenarios for testing

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
scenarioYesTest scenario to run

Implementation Reference

  • The execute function implementing the tool logic. It defines multiple test scenarios and generates formatted test plans with Swift code snippets for LDK testing based on the input scenario.
      execute: async (args: any): Promise<ToolResult> => {
        const scenarios: Record<string, any> = {
          basic_payment: {
            title: 'Basic Lightning Payment Test',
            description: 'Test a simple payment between two nodes',
            steps: [
              {
                step: 1,
                action: 'Setup nodes',
                code: `// Initialize two LDK nodes
    let alice = try await LDKNode(name: "Alice", network: .regtest)
    let bob = try await LDKNode(name: "Bob", network: .regtest)
    
    // Connect nodes
    try await alice.connectToPeer(
        pubkey: bob.nodeId,
        address: "127.0.0.1",
        port: 9735
    )`
              },
              {
                step: 2,
                action: 'Open channel',
                code: `// Alice opens channel to Bob
    let channelId = try await alice.openChannel(
        peerPubkey: bob.nodeId,
        amountSats: 1_000_000,
        pushSats: 100_000
    )
    
    // Wait for channel confirmation
    try await alice.waitForChannelReady(channelId: channelId)`
              },
              {
                step: 3,
                action: 'Create invoice',
                code: `// Bob creates invoice
    let invoice = try await bob.createInvoice(
        amountSats: 10_000,
        description: "Test payment"
    )
    print("Invoice: \\(invoice.bolt11)")`
              },
              {
                step: 4,
                action: 'Send payment',
                code: `// Alice pays invoice
    let paymentResult = try await alice.payInvoice(invoice.bolt11)
    assert(paymentResult.status == .succeeded)
    print("Payment sent! Preimage: \\(paymentResult.preimage)")`
              },
              {
                step: 5,
                action: 'Verify payment',
                code: `// Verify balances updated
    let aliceBalance = try await alice.getBalance()
    let bobBalance = try await bob.getBalance()
    
    assert(aliceBalance.totalSats < 900_000) // Deducted payment + fees
    assert(bobBalance.totalSats > 109_000) // Received payment`
              }
            ],
            expectedResults: [
              'Channel opens successfully',
              'Payment completes within 5 seconds',
              'Balances update correctly',
              'Payment events fire properly'
            ]
          },
    
          channel_lifecycle: {
            title: 'Complete Channel Lifecycle Test',
            description: 'Test opening, using, and closing a channel',
            steps: [
              {
                step: 1,
                action: 'Open channel',
                code: `// Open channel with specific parameters
    let config = ChannelConfig(
        isPublic: false,
        minDepth: 3,
        maxHtlcCount: 10,
        forceCloseAvoidanceMaxFeeSats: 1000
    )
    
    let channel = try await node.openChannel(
        peerPubkey: peerNode.nodeId,
        amountSats: 500_000,
        config: config
    )`
              },
              {
                step: 2,
                action: 'Send payments',
                code: `// Send multiple payments
    for i in 1...5 {
        let invoice = try await peerNode.createInvoice(
            amountSats: UInt64(i * 1000),
            description: "Payment \\(i)"
        )
        
        try await node.payInvoice(invoice.bolt11)
        print("Payment \\(i) completed")
    }`
              },
              {
                step: 3,
                action: 'Check channel state',
                code: `// Monitor channel health
    let channelInfo = try await node.getChannel(channelId: channel.id)
    print("Local balance: \\(channelInfo.localBalanceSats)")
    print("Remote balance: \\(channelInfo.remoteBalanceSats)")
    print("Total sent: \\(channelInfo.totalSatsSent)")
    print("Total received: \\(channelInfo.totalSatsReceived)")`
              },
              {
                step: 4,
                action: 'Cooperative close',
                code: `// Close channel cooperatively
    try await node.closeChannel(
        channelId: channel.id,
        targetFeeSatsPerVbyte: 5
    )
    
    // Wait for close transaction
    try await node.waitForChannelClose(channelId: channel.id)
    
    // Verify funds returned to wallet
    let finalBalance = try await node.getOnChainBalance()
    assert(finalBalance > 490_000) // Most funds returned minus fees`
              }
            ]
          },
    
          multi_hop_payment: {
            title: 'Multi-Hop Payment Test',
            description: 'Test payment routing through multiple nodes',
            setup: `// Setup: Alice -> Bob -> Carol -> Dave
    let nodes = try await createNodeNetwork(["Alice", "Bob", "Carol", "Dave"])
    
    // Open channels in sequence
    try await openChannel(from: nodes[0], to: nodes[1], sats: 1_000_000)
    try await openChannel(from: nodes[1], to: nodes[2], sats: 1_000_000)
    try await openChannel(from: nodes[2], to: nodes[3], sats: 1_000_000)`,
            test: `// Dave creates invoice
    let invoice = try await nodes[3].createInvoice(
        amountSats: 50_000,
        description: "Multi-hop test"
    )
    
    // Alice finds route and pays
    let route = try await nodes[0].findRoute(
        to: nodes[3].nodeId,
        amountSats: 50_000
    )
    
    print("Route: \\(route.hops.map { $0.nodeId.prefix(8) }.joined(separator: " -> "))")
    print("Total fees: \\(route.totalFeeSats) sats")
    
    let payment = try await nodes[0].payInvoice(invoice.bolt11)
    assert(payment.status == .succeeded)`,
            validation: [
              'Payment routes through all 3 hops',
              'Each node earns routing fees',
              'Payment completes in < 10 seconds'
            ]
          },
    
          payment_failure: {
            title: 'Payment Failure Handling Test',
            description: 'Test various payment failure scenarios',
            scenarios: [
              {
                name: 'Insufficient liquidity',
                code: `// Try to pay more than channel capacity
    let invoice = try await createLargeInvoice(sats: 2_000_000)
    
    do {
        try await node.payInvoice(invoice)
        XCTFail("Payment should have failed")
    } catch PaymentError.insufficientBalance {
        print("✓ Correctly failed with insufficient balance")
    }`
              },
              {
                name: 'Invalid invoice',
                code: `// Test expired invoice
    let expiredInvoice = "lnbc1..." // Old invoice
    
    do {
        try await node.payInvoice(expiredInvoice)
        XCTFail("Should fail on expired invoice")
    } catch PaymentError.invoiceExpired {
        print("✓ Correctly rejected expired invoice")
    }`
              },
              {
                name: 'Route not found',
                code: `// Try to pay disconnected node
    let isolatedNode = try await createIsolatedNode()
    let invoice = try await isolatedNode.createInvoice(sats: 1000)
    
    do {
        try await node.payInvoice(invoice.bolt11)
        XCTFail("Should fail to find route")
    } catch PaymentError.noRoute {
        print("✓ Correctly failed to find route")
    }`
              }
            ]
          },
    
          force_close: {
            title: 'Force Close Channel Test',
            description: 'Test unilateral channel closure and fund recovery',
            steps: [
              {
                action: 'Setup channel with pending HTLCs',
                code: `// Create channel with in-flight payments
    let channel = try await alice.openChannel(to: bob, sats: 1_000_000)
    
    // Create pending payment (don't claim yet)
    let preimage = generatePreimage()
    let paymentHash = sha256(preimage)
    let invoice = bob.createHoldInvoice(
        paymentHash: paymentHash,
        amountSats: 50_000
    )
    
    // Alice sends but Bob doesn't claim
    Task {
        try await alice.payInvoice(invoice)
    }`
              },
              {
                action: 'Force close channel',
                code: `// Bob goes offline
    bob.disconnect()
    
    // Alice force closes
    let closeTx = try await alice.forceCloseChannel(channelId: channel.id)
    print("Force close tx: \\(closeTx.txid)")
    
    // Wait for confirmation
    try await alice.waitForTransaction(txid: closeTx.txid)`
              },
              {
                action: 'Claim funds after timeout',
                code: `// Wait for CSV timeout (144 blocks on regtest)
    try await mineBlocks(count: 144)
    
    // Alice claims her funds
    let claimTx = try await alice.claimForceCloseOutputs()
    print("Claimed funds in tx: \\(claimTx.txid)")
    
    // Verify funds recovered
    let balance = try await alice.getOnChainBalance()
    assert(balance > 940_000) // Most funds recovered minus fees`
              }
            ]
          },
    
          offline_receive: {
            title: 'Offline Payment Receive Test',
            description: 'Test receiving payments while offline',
            implementation: `// Generate invoices while online
    let invoices = try await (0..<5).asyncMap { i in
        try await node.createInvoice(
            amountSats: 10_000,
            description: "Offline test \\(i)"
        )
    }
    
    // Go offline
    node.disconnect()
    print("Node offline, invoices still valid")
    
    // Payer sends to invoices (will be pending)
    for invoice in invoices {
        Task {
            try? await payerNode.payInvoice(invoice.bolt11)
        }
    }
    
    // Come back online
    try await node.connect()
    print("Node back online")
    
    // Process pending payments
    let received = try await node.processPendingPayments()
    print("Received \\(received.count) payments while offline")
    
    // Verify all payments received
    for payment in received {
        assert(payment.status == .succeeded)
        assert(payment.amountSats == 10_000)
    }`,
            notes: [
              'Payments are held by sender until receiver comes online',
              'HTLCs have timeout, so extended offline periods may fail',
              'Watchtowers can help claim funds while offline'
            ]
          },
    
          fee_spike: {
            title: 'Fee Spike Handling Test',
            description: 'Test behavior during high on-chain fee periods',
            scenario: `// Simulate fee spike
    mockFeeEstimator.setFeeRate(.high, satsPerVbyte: 200)
    
    // Test channel operations
    let tests = [
        "Opening channels becomes expensive",
        "Force close protections activate",
        "Anchor outputs allow fee bumping",
        "Routing fees may increase"
    ]
    
    // Test opening channel with high fees
    do {
        try await node.openChannel(
            to: peer,
            sats: 100_000,
            targetConf: 6
        )
    } catch ChannelError.feesTooHigh(let estimatedFee) {
        print("Channel open would cost \\(estimatedFee) sats in fees")
        
        // Wait for lower fees or use anchor outputs
        let config = ChannelConfig(useAnchors: true)
        try await node.openChannel(
            to: peer,
            sats: 100_000,
            config: config
        )
    }`,
            adaptations: [
              'Use anchor outputs for fee flexibility',
              'Batch operations when possible',
              'Prefer cooperative closes',
              'Implement fee estimation warnings in UI'
            ]
          },
    
          backup_restore: {
            title: 'Backup and Restore Test',
            description: 'Test complete wallet backup and restoration',
            steps: [
              {
                action: 'Create backup',
                code: `// Backup all critical data
    let backup = try await node.createBackup()
    
    let backupData = BackupData(
        seed: backup.seed,
        channelMonitors: backup.channelMonitors,
        channelManager: backup.channelManager,
        timestamp: Date()
    )
    
    // Encrypt and save
    let encrypted = try backupData.encrypt(password: "testpass")
    try encrypted.write(to: backupURL)`
              },
              {
                action: 'Simulate data loss',
                code: `// Clear all local data
    try FileManager.default.removeItem(at: ldkDataDirectory)
    
    // Verify node cannot start
    do {
        try await LDKNode.load(from: ldkDataDirectory)
        XCTFail("Should not load without data")
    } catch {
        print("✓ Correctly failed to load")`
              },
              {
                action: 'Restore from backup',
                code: `// Load and decrypt backup
    let encryptedData = try Data(contentsOf: backupURL)
    let backup = try BackupData.decrypt(
        data: encryptedData,
        password: "testpass"
    )
    
    // Restore node
    let restoredNode = try await LDKNode.restore(
        from: backup,
        network: .regtest
    )
    
    // Verify channels restored
    let channels = try await restoredNode.listChannels()
    assert(channels.count == originalChannelCount)
    
    // Force close if peer is gone
    for channel in channels {
        if !channel.isUsable {
            try await restoredNode.forceCloseChannel(
                channelId: channel.id
            )
        }
    }`
              }
            ],
            criticalData: [
              'Seed phrase (encrypted)',
              'Channel monitors (latest state)',
              'Channel manager state',
              'Network graph (optional)',
              'Payment history (optional)'
            ]
          }
        };
    
        try {
          const scenario = scenarios[args.scenario];
          if (!scenario) {
            throw new Error(`Unknown scenario: ${args.scenario}`);
          }
    
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                success: true,
                scenario: args.scenario,
                testPlan: scenario,
                runCommand: `
    // To run this scenario in your iOS app:
    // 1. Create a test target in Xcode
    // 2. Import the scenario code
    // 3. Run with: cmd+U or 'xcodebuild test'
    
    // Example test file:
    import XCTest
    import LightningDevKit
    @testable import LightningWallet
    
    class ${args.scenario.charAt(0).toUpperCase() + args.scenario.slice(1).replace(/_/g, '')}Tests: XCTestCase {
        func test${args.scenario.charAt(0).toUpperCase() + args.scenario.slice(1).replace(/_/g, '')}() async throws {
            // Scenario implementation here
        }
    }`.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
          };
        }
      }
  • The input schema defining the required 'scenario' parameter with an enum of available LDK test scenarios.
    inputSchema: {
      type: 'object',
      properties: {
        scenario: {
          type: 'string',
          enum: [
            'basic_payment',
            'channel_lifecycle',
            'multi_hop_payment',
            'payment_failure',
            'force_close',
            'offline_receive',
            'fee_spike',
            'backup_restore'
          ],
          description: 'Test scenario to run'
        }
      },
      required: ['scenario']
    },
  • src/index.ts:32-62 (registration)
    The tool is imported from './tools/testScenario.js' and added to the 'tools' array which is used by the MCP server to list and execute tools.
    import { testScenarioTool } from './tools/testScenario.js';
    import { networkGraphTool } from './tools/networkGraph.js';
    import { eventHandlingTool } from './tools/eventHandling.js';
    import { chainSyncTool } from './tools/chainSync.js';
    
    // Aggregate all 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