Skip to content

πŸŽ›οΈfeat(mcp): add orchestrate-escrow tool β€” single MCP command drives the full deploy β†’ fund β†’ release lifecycleΒ #415

Description

@sotoJ24

Title

feat(mcp): add orchestrate-escrow tool β€” single MCP command drives the full deploy β†’ fund β†’ release lifecycle, guiding Claude through each XDR signing step

Issue Summary

Currently a developer using the SafeTrust MCP must call deploy-escrow, then manually handle the XDR signing in Freighter, then call fund-escrow, then sign again. There is no single tool that explains the full sequence and tracks state across steps. This issue adds orchestrate-escrow β€” a stateless tool that accepts a step parameter and returns exactly what to do next, including the XDR that needs signing and the subsequent MCP tool call to make. It does not introduce state β€” each call is independent and the caller passes engagementId and contractId to resume a sequence.

Type of Issue

  • Feature Request

Branch strategy

βœ… feat/issue-N-orchestrate-escrow β†’ consolidation-pattern
❌ feat/issue-N-orchestrate-escrow β†’ main

Current Behavior

Developer workflow (3 separate tool calls + manual XDR handling):
1. MCP: deploy-escrow β†’ gets unsignedXDR
2. Developer: manually opens Freighter, signs XDR, gets signedXDR
3. MCP: (no submit tool) β†’ developer calls frontend send-transaction manually
4. MCP: fund-escrow β†’ gets another unsignedXDR
5. Developer: signs again in Freighter
6. MCP: (no release tool) β†’ manual again

Expected Behavior

mcp/src/tools/
β”œβ”€β”€ apartments.ts     ← unchanged
β”œβ”€β”€ escrow.ts         ← add orchestrate-escrow tool here
└── hasura.ts         ← unchanged

New orchestrate-escrow tool

server.registerTool(
  'orchestrate-escrow',
  {
    title: 'Orchestrate escrow',
    description:
      'Step-by-step guide for the full SafeTrust escrow lifecycle. ' +
      'Call with step="start" to begin, then follow the "next" instruction ' +
      'in each response. Stateless β€” pass engagementId and contractId from ' +
      'previous steps to resume.',
    inputSchema: z.object({
      step: z.enum(['start', 'after-deploy', 'after-fund', 'status'])
        .describe('Current step in the lifecycle'),
      apartmentId: z.string().uuid().optional(),
      senderAddress: stellarAddress.optional(),
      receiverAddress: stellarAddress.optional(),
      amount: z.number().positive().optional(),
      engagementId: z.string().optional(),
      contractId: z.string().optional(),
    }),
  },
  async ({ step, apartmentId, senderAddress, receiverAddress, amount, engagementId, contractId }) => {
    switch (step) {
      case 'start':
        if (!apartmentId || !senderAddress || !receiverAddress || !amount) {
          return errorResult(
            'For step=start, provide: apartmentId, senderAddress, receiverAddress, amount'
          );
        }
        return textResult(
          '── Step 1: Deploy escrow ──────────────────────────────────',
          'Call deploy-escrow with these parameters:',
          jsonBlock({ apartmentId, senderAddress, receiverAddress, amount }),
          '',
          'deploy-escrow will return an unsignedXDR.',
          'Sign it with Freighter in the browser.',
          'Then call orchestrate-escrow again with:',
          '  step: "after-deploy"',
          '  engagementId: (from deploy-escrow response)',
          '  contractId: (from deploy-escrow response)',
          '  senderAddress: (same as above)',
          '  amount: (same as above)',
        );

      case 'after-deploy':
        if (!contractId || !senderAddress || !amount) {
          return errorResult(
            'For step=after-deploy, provide: contractId, senderAddress, amount'
          );
        }
        return textResult(
          '── Step 2: Fund escrow ────────────────────────────────────',
          `contractId: ${contractId}`,
          '',
          'The deploy XDR has been signed and submitted.',
          'Now call fund-escrow with:',
          jsonBlock({ contractId, signer: senderAddress, amount }),
          '',
          'fund-escrow returns another unsignedXDR.',
          'Sign it with Freighter.',
          'Then call orchestrate-escrow with step="after-fund" to continue.',
        );

      case 'after-fund':
        return textResult(
          '── Step 3: Escrow funded ──────────────────────────────────',
          'The escrow is now funded and locked on Stellar.',
          '',
          'When the host completes the service:',
          '  1. Host marks milestone done (POST /api/escrow/milestone-status)',
          '  2. Tenant calls release-funds (POST /api/escrow/release-funds)',
          '',
          'To check current status:',
          '  call get-escrow-status with contractId or engagementId',
          '',
          escrowRolesDoc(),
        );

      case 'status':
        if (!contractId && !engagementId) {
          return errorResult('Provide contractId or engagementId to check status.');
        }
        return textResult(
          'Call get-escrow-status with:',
          jsonBlock({ contractId, engagementId }),
        );
    }
  },
);

Consolidation pattern alignment

This tool respects the Compute Resource Consolidation pattern:

  • orchestrate-escrow with step=start β†’ guides caller to use deploy-escrow
  • deploy-escrow β†’ calls apps/api POST /api/escrow/deploy β†’ calls TrustlessWork
  • MCP never calls TrustlessWork directly
  • XDR signing always happens in the browser (Freighter) β€” never server-side

Files to create / modify

File Status Action
mcp/src/tools/escrow.ts βœ… exists Add orchestrate-escrow tool
mcp/README.md βœ… exists Document the orchestration flow

Acceptance Criteria

  • orchestrate-escrow step=start without required fields returns clear error
  • orchestrate-escrow step=start with all fields returns correct next-step instructions
  • orchestrate-escrow step=after-deploy returns fund-escrow parameters
  • Tool never calls TrustlessWork directly β€” always routes through apps/api
  • pnpm --filter @safetrust/mcp test still passes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions