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
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
Title
feat(mcp): add orchestrate-escrow tool β single MCP command drives the full deploy β fund β release lifecycle, guiding Claude through each XDR signing stepIssue Summary
Currently a developer using the SafeTrust MCP must call
deploy-escrow, then manually handle the XDR signing in Freighter, then callfund-escrow, then sign again. There is no single tool that explains the full sequence and tracks state across steps. This issue addsorchestrate-escrowβ a stateless tool that accepts astepparameter 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 passesengagementIdandcontractIdto resume a sequence.Type of Issue
Branch strategy
Current Behavior
Expected Behavior
New
orchestrate-escrowtoolConsolidation pattern alignment
This tool respects the Compute Resource Consolidation pattern:
orchestrate-escrowwithstep=startβ guides caller to usedeploy-escrowdeploy-escrowβ callsapps/api POST /api/escrow/deployβ calls TrustlessWorkFiles to create / modify
mcp/src/tools/escrow.tsorchestrate-escrowtoolmcp/README.mdAcceptance Criteria
orchestrate-escrow step=startwithout required fields returns clear errororchestrate-escrow step=startwith all fields returns correct next-step instructionsorchestrate-escrow step=after-deployreturns fund-escrow parameterspnpm --filter @safetrust/mcp teststill passes