Skip to main content
v2

MCP Example 2 - Loan Application (Conditional API Chain)

This example demonstrates a real-world conditional MCP chain:

  1. call Credit Union Rating API
  2. if creditRating > 750, call Credit Card Fraud API
  3. if not flagged, call Debt/Credit Summary API
  4. if debt profile is acceptable, call Loan Submit API

All APIs are mocked through mockey with response delays to simulate real external systems.

Repos and files used

Mock APIs (mockey)

  • GET /api/mock/loan/credit-union/rating
  • GET /api/mock/loan/credit-card/fraud-check
  • GET /api/mock/loan/debt-credit/summary
  • POST /api/mock/loan/application/submit

Key files:

  • mockey/src/resolver/provider/live-api.resolver.js
  • mockey/src/resolver/core/resolver.registry.js
  • mockey/src/route/mockey-route.json
  • mockey/src/response/loan/*.json (includes delay via responseDelayInMillis)

Demo MCP handlers (convengine-demo)

  • loan.credit.rating.check
  • loan.credit.fraud.check
  • loan.debt.credit.summary
  • loan.application.submit

Handler files:

  • convengine-demo/.../LoanCreditRatingToolHandler.java
  • convengine-demo/.../LoanFraudCheckToolHandler.java
  • convengine-demo/.../LoanDebtSummaryToolHandler.java
  • convengine-demo/.../LoanApplicationSubmitToolHandler.java

Seed packs

  • Postgres: convengine-demo/src/main/resources/sql/example2_seed.sql
  • SQLite: convengine-demo/src/main/resources/sql/example2_sqlite_seed.sql

These seed files include all required DML:

  • ce_intent
  • ce_intent_classifier
  • ce_output_schema
  • ce_prompt_template
  • ce_response
  • ce_rule
  • ce_mcp_tool (with intent_code + state_code)
  • ce_mcp_planner scoped prompt rows (default + LOAN_APPLICATION/ELIGIBILITY_GATE)

Required rule/response rows for this flow

Use these key rows so classifier + MCP + response resolution line up correctly:

-- 1) Bootstrap state after classifier picks LOAN_APPLICATION
INSERT INTO ce_rule (intent_code, state_code, rule_type, match_pattern, action, action_value, phase, priority, enabled, description)
VALUES
('LOAN_APPLICATION', 'UNKNOWN', 'REGEX', '.*', 'SET_STATE', 'ELIGIBILITY_GATE', 'POST_AGENT_INTENT', 20, true,
'Move LOAN_APPLICATION from UNKNOWN into ELIGIBILITY_GATE.');

-- 2) After MCP planner returns ANSWER, close state for final response mapping
INSERT INTO ce_rule (intent_code, state_code, rule_type, match_pattern, action, action_value, phase, priority, enabled, description)
VALUES
('LOAN_APPLICATION', 'ELIGIBILITY_GATE', 'JSON_PATH', '$[?(@.context.mcp.finalAnswer != null && @.context.mcp.finalAnswer != '''')]', 'SET_STATE', 'COMPLETED', 'POST_AGENT_MCP', 30, true,
'Move loan flow to COMPLETED only when context.mcp.finalAnswer exists.');

-- 3) Final response row should target COMPLETED and derive from MCP context
INSERT INTO ce_response (intent_code, state_code, output_format, response_type, derivation_hint, priority, enabled, description)
VALUES
('LOAN_APPLICATION', 'COMPLETED', 'TEXT', 'DERIVED',
'Use context.mcp.finalAnswer as primary summary. Validate with context.mcp.observations (rating/fraud/debt/submit). Do not invent values.',
20, true, 'Loan decision derived from MCP outputs.');

-- 4) Prompt template should explicitly read context.mcp.*
INSERT INTO ce_prompt_template (intent_code, state_code, response_type, system_prompt, user_prompt, interaction_mode, interaction_contract, enabled)
VALUES
('LOAN_APPLICATION', 'COMPLETED', 'TEXT',
'You are a precise loan workflow summarizer. Use only MCP evidence from context JSON.',
'Context JSON:\n{{context}}\n\nRead context.mcp.observations and context.mcp.finalAnswer. Return a concise final loan decision.',
'FINAL',
'{"allows":["reset"],"expects":[]}',
true);

Step-by-step test

1) Start mockey

cd /Users/salilvnair/workspace/git/salilvnair/mockey
npm install
npm start

Expected:

listening to 31333

2) Optional quick mock checks

curl -s "http://localhost:31333/api/mock/loan/credit-union/rating?customerId=CUST-1001"
curl -s "http://localhost:31333/api/mock/loan/credit-card/fraud-check?customerId=CUST-1001"
curl -s "http://localhost:31333/api/mock/loan/debt-credit/summary?customerId=CUST-1001"
curl -s -X POST "http://localhost:31333/api/mock/loan/application/submit" -H "Content-Type: application/json" -d '{"customerId":"CUST-1001","requestedAmount":350000,"tenureMonths":36}'

3) Start convengine-demo

cd /Users/salilvnair/workspace/git/salilvnair/convengine-demo
./mvnw spring-boot:run

4) Seed Example 2 DML

For Postgres run:

  • convengine-demo/src/main/resources/sql/example2_seed.sql

For SQLite run:

  • convengine-demo/src/main/resources/sql/example2_sqlite_seed.sql

5) Run as chat-style walkthrough

Turn 1 - User
Apply loan for customer CUST-1001, amount 350000 for 36 months.
Turn 1 - Assistant (internal MCP)
intent: LOAN_APPLICATIONstate: ELIGIBILITY_GATE
Running eligibility chain: rating -> fraud -> debt summary -> submit.
Turn 1 - Final Assistant Output
intent: LOAN_APPLICATIONstate: COMPLETED
Loan application submitted successfully. Application ID: LA-90311. Credit and fraud checks passed; debt profile is within threshold.

6) Verify audit stages

Look for:

  • MCP_PLAN_LLM_INPUT
  • MCP_PLAN_LLM_OUTPUT
  • MCP_TOOL_CALL
  • MCP_TOOL_RESULT
  • MCP_FINAL_ANSWER

Also for direct tool requests:

  • TOOL_ORCHESTRATION_REQUEST
  • TOOL_ORCHESTRATION_RESULT

7) Verify advanced HTTP execution metadata

Mapped output includes framework execution details:

{
"status": 200,
"attempt": 1,
"latencyMs": 1205,
"mapped": {
"customerId": "CUST-1001",
"creditRating": 782
}
}

The latency reflects mock delays (responseDelayInMillis) configured in mockey responses.

How MCP Tool Output Is Used in Response Resolution (Step-by-Step)

For this example, the final assistant text is not hardcoded. It is derived from MCP evidence in runtime context:

  1. McpToolStep starts and clears stale context.mcp.finalAnswer and context.mcp.observations.
  2. Every successful tool call appends one entry into context.mcp.observations:
    • toolCode
    • json (stringified mapped payload)
  3. When planner returns ANSWER, McpToolStep writes:
    • context.mcp.finalAnswer
    • input param mcp_final_answer
  4. In the confirmation-first pattern, POST_SCHEMA_EXTRACTION can move the flow to CONFIRMATION, PRE_AGENT_MCP can move CONFIRMATION -> PROCESS_APPLICATION, and POST_AGENT_MCP can set COMPLETED when context.mcp.finalAnswer is present.
  5. ResponseResolutionStep selects ce_response for:
    • intent_code=LOAN_APPLICATION
    • state_code=COMPLETED
    • response type DERIVED, output format TEXT
  6. It then selects matching ce_prompt_template (TEXT) for the same intent/state.
  7. TextOutputFormatResolver invokes LLM with:
    • rendered system/user prompt
    • ce_response.derivation_hint
    • context payload (session.contextDict()), which includes MCP observations/finalAnswer
  8. LLM produces final summary text. Engine audits RESOLVE_RESPONSE_LLM_OUTPUT and ASSISTANT_OUTPUT, then persists conversation.
Practical implication

In this setup, the response LLM is grounded by two things together: derivation_hint policy from ce_response and MCP evidence from context.mcp.*. That is why the final answer reflects rating/fraud/debt/applicationId consistently.

The provided Example 2 seed now makes this explicit in SQL as well:

  • ce_prompt_template.user_prompt explicitly instructs reading context.mcp.observations and context.mcp.finalAnswer
  • ce_response.derivation_hint explicitly references context.mcp.*

E2E Turn-by-Turn (Tab View)

Single request turn (approved branch)

Loop/StepLLM deductionTables touchedProduced state/data
Schema extractioncustomerId/requestedAmount/tenure extracted.ce_output_schema(R), ce_prompt_template(R), ce_audit(W)context fields set
State ruleloan flow can move ELIGIBILITY_GATE -> CONFIRMATION after schema extraction, then PRE_AGENT_MCP moves CONFIRMATION -> PROCESS_APPLICATION before MCP.ce_rule(R), ce_audit(W)state confirmation gate before MCP
MCP #1Need credit rating first.ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W)CALL_TOOL rating
Tool #1Rating available and > 750.ce_audit(W)obs[0]=creditRating
MCP #2Proceed to fraud check.ce_mcp_planner(R), ce_audit(W)CALL_TOOL fraud
Tool #2Fraud clear.ce_audit(W)obs[1]=flagged:false
MCP #3Need affordability metrics.ce_mcp_planner(R), ce_audit(W)CALL_TOOL debt summary
Tool #3DTI acceptable and credit sufficient.ce_audit(W)obs[2]=dti/availableCredit
MCP #4Submit application now.ce_mcp_planner(R), ce_audit(W)CALL_TOOL submit
Tool #4Submission succeeded.ce_audit(W)obs[3]=applicationId/status
MCP ANSWEREnough evidence for final decision.ce_audit(W)context.mcp.finalAnswer
ResponseResolutionStepGenerate final user-facing summary.ce_response(R), ce_prompt_template(R), ce_audit(W)assistant text payload

ReactFlow Execution Map

Example 2 End-to-End Execution

Conditional MCP chain from eligibility checks to final response resolution.

React Flow mini map

Response Field Provenance

Where final response facts come from

Response factPrimary sourceStage where it enters context
creditRatingloan.credit.rating.check mapped payloadMCP_TOOL_RESULT #1
fraudFlagloan.credit.fraud.check mapped payloadMCP_TOOL_RESULT #2
dti / availableCreditloan.debt.credit.summary mapped payloadMCP_TOOL_RESULT #3
applicationId / submitStatusloan.application.submit mapped payloadMCP_TOOL_RESULT #4 (approved path)
Final decision sentenceplanner answer + response derivationMCP_FINAL_ANSWER + RESOLVE_RESPONSE_LLM_OUTPUT

Notes on state and scope

ce_mcp_tool rows in example2 are scoped as:

  • intent_code = LOAN_APPLICATION
  • state_code = ELIGIBILITY_GATE

This ensures tools are visible only in the loan decision state.

Troubleshooting

  • Tool not resolved:
    • verify tool_code in DB matches handler toolCode() exactly
  • No tool call in planner path:
    • check McpPlanner prompt rows in ce_mcp_planner
    • confirm active intent is LOAN_APPLICATION
  • Timeouts/retries unexpected:
    • inspect handler-specific HttpApiExecutionPolicy
    • inspect mock delay values in mockey/src/response/loan/*.json