MCP Example 2 - Loan Application (Conditional API Chain)
This example demonstrates a real-world conditional MCP chain:
- call Credit Union Rating API
- if
creditRating > 750, call Credit Card Fraud API - if not flagged, call Debt/Credit Summary API
- 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/ratingGET /api/mock/loan/credit-card/fraud-checkGET /api/mock/loan/debt-credit/summaryPOST /api/mock/loan/application/submit
Key files:
mockey/src/resolver/provider/live-api.resolver.jsmockey/src/resolver/core/resolver.registry.jsmockey/src/route/mockey-route.jsonmockey/src/response/loan/*.json(includes delay viaresponseDelayInMillis)
Demo MCP handlers (convengine-demo)
loan.credit.rating.checkloan.credit.fraud.checkloan.debt.credit.summaryloan.application.submit
Handler files:
convengine-demo/.../LoanCreditRatingToolHandler.javaconvengine-demo/.../LoanFraudCheckToolHandler.javaconvengine-demo/.../LoanDebtSummaryToolHandler.javaconvengine-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_intentce_intent_classifierce_output_schemace_prompt_templatece_responsece_rulece_mcp_tool(withintent_code+state_code)ce_mcp_plannerscoped 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
- Conversation
- Turn Table
- Request Payloads
Loan flow summary
| Phase | What happens | Tables touched | Output |
|---|---|---|---|
| Schema + state | Extract fields and switch to ELIGIBILITY_GATE. | ce_output_schema(R), ce_rule(R), ce_audit(W) | context fields + state |
| Post-MCP transition | POST_AGENT_MCP rule moves state to COMPLETED only when context.mcp.finalAnswer is present. | ce_rule(R), ce_audit(W) | state=COMPLETED |
| MCP tool chain | Rating, fraud, debt, submit executed conditionally. | ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W) | context.mcp.observations[] |
| MCP final answer | Planner writes final decision text. | ce_audit(W) | context.mcp.finalAnswer |
| Response resolution | DERIVED TEXT generated for user. | ce_response(R), ce_prompt_template(R), ce_audit(W) | assistant payload |
Planner mode payload:
{
"userInput": "Apply loan for customer CUST-1001, amount 350000 for 36 months",
"contextJson": "{}",
"inputParams": {}
}
Direct branch payload (low rating):
{
"userInput": "Apply loan",
"contextJson": "{}",
"inputParams": {
"tool_request": {
"tool_code": "loan.credit.rating.check",
"tool_group": "HTTP_API",
"args": { "customerId": "CUST-LOW" }
}
}
}
Direct branch payload (fraud):
{
"userInput": "Apply loan",
"contextJson": "{}",
"inputParams": {
"tool_request": {
"tool_code": "loan.credit.fraud.check",
"tool_group": "HTTP_API",
"args": { "customerId": "CUST-FRAUD" }
}
}
}
6) Verify audit stages
Look for:
MCP_PLAN_LLM_INPUTMCP_PLAN_LLM_OUTPUTMCP_TOOL_CALLMCP_TOOL_RESULTMCP_FINAL_ANSWER
Also for direct tool requests:
TOOL_ORCHESTRATION_REQUESTTOOL_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:
McpToolStepstarts and clears stalecontext.mcp.finalAnswerandcontext.mcp.observations.- Every successful tool call appends one entry into
context.mcp.observations:toolCodejson(stringified mapped payload)
- When planner returns
ANSWER,McpToolStepwrites:context.mcp.finalAnswer- input param
mcp_final_answer
- In the confirmation-first pattern,
POST_SCHEMA_EXTRACTIONcan move the flow toCONFIRMATION,PRE_AGENT_MCPcan moveCONFIRMATION -> PROCESS_APPLICATION, andPOST_AGENT_MCPcan setCOMPLETEDwhencontext.mcp.finalAnsweris present. ResponseResolutionStepselectsce_responsefor:intent_code=LOAN_APPLICATIONstate_code=COMPLETED- response type
DERIVED, output formatTEXT
- It then selects matching
ce_prompt_template(TEXT) for the same intent/state. TextOutputFormatResolverinvokes LLM with:- rendered system/user prompt
ce_response.derivation_hint- context payload (
session.contextDict()), which includes MCP observations/finalAnswer
- LLM produces final summary text. Engine audits
RESOLVE_RESPONSE_LLM_OUTPUTandASSISTANT_OUTPUT, then persists conversation.
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_promptexplicitly instructs readingcontext.mcp.observationsandcontext.mcp.finalAnswerce_response.derivation_hintexplicitly referencescontext.mcp.*
E2E Turn-by-Turn (Tab View)
- Approved Path
- Low Rating
- Fraud Flagged
- High Debt
Single request turn (approved branch)
| Loop/Step | LLM deduction | Tables touched | Produced state/data |
|---|---|---|---|
| Schema extraction | customerId/requestedAmount/tenure extracted. | ce_output_schema(R), ce_prompt_template(R), ce_audit(W) | context fields set |
| State rule | loan 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 #1 | Need credit rating first. | ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W) | CALL_TOOL rating |
| Tool #1 | Rating available and > 750. | ce_audit(W) | obs[0]=creditRating |
| MCP #2 | Proceed to fraud check. | ce_mcp_planner(R), ce_audit(W) | CALL_TOOL fraud |
| Tool #2 | Fraud clear. | ce_audit(W) | obs[1]=flagged:false |
| MCP #3 | Need affordability metrics. | ce_mcp_planner(R), ce_audit(W) | CALL_TOOL debt summary |
| Tool #3 | DTI acceptable and credit sufficient. | ce_audit(W) | obs[2]=dti/availableCredit |
| MCP #4 | Submit application now. | ce_mcp_planner(R), ce_audit(W) | CALL_TOOL submit |
| Tool #4 | Submission succeeded. | ce_audit(W) | obs[3]=applicationId/status |
| MCP ANSWER | Enough evidence for final decision. | ce_audit(W) | context.mcp.finalAnswer |
| ResponseResolutionStep | Generate final user-facing summary. | ce_response(R), ce_prompt_template(R), ce_audit(W) | assistant text payload |
Early-stop branch: low credit score
| Loop/Step | LLM deduction | Tables touched | Produced state/data |
|---|---|---|---|
| MCP #1 | Call rating check. | ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W) | CALL_TOOL rating |
| Tool #1 | creditRating <= 750. | ce_audit(W) | obs[0]=low rating |
| MCP ANSWER | Stop chain; reject without fraud/debt/submit. | ce_mcp_planner(R), ce_audit(W) | context.mcp.finalAnswer (rejection) |
| ResponseResolutionStep | Render low-score rejection summary. | ce_response(R), ce_prompt_template(R), ce_audit(W) | assistant text payload |
Branch: fraud hit after good rating
| Loop/Step | LLM deduction | Tables touched | Produced state/data |
|---|---|---|---|
| MCP #1 + Tool #1 | Rating is high enough to continue. | ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W) | obs[0]=rating |
| MCP #2 + Tool #2 | Fraud flagged=true. | ce_mcp_planner(R), ce_audit(W) | obs[1]=fraud flagged |
| MCP ANSWER | Stop chain immediately; reject. | ce_mcp_planner(R), ce_audit(W) | context.mcp.finalAnswer (fraud rejection) |
| ResponseResolutionStep | Produce fraud-specific final text. | ce_response(R), ce_prompt_template(R), ce_audit(W) | assistant text payload |
Branch: affordability fails after fraud clears
| Loop/Step | LLM deduction | Tables touched | Produced state/data |
|---|---|---|---|
| MCP #1 + Tool #1 | Rating allows continuation. | ce_mcp_tool(R), ce_mcp_planner(R), ce_audit(W) | obs[0]=rating |
| MCP #2 + Tool #2 | Fraud clear. | ce_mcp_planner(R), ce_audit(W) | obs[1]=fraud false |
| MCP #3 + Tool #3 | DTI too high or available credit too low. | ce_mcp_planner(R), ce_audit(W) | obs[2]=poor affordability |
| MCP ANSWER | Reject or mark manual review; skip submit. | ce_mcp_planner(R), ce_audit(W) | context.mcp.finalAnswer |
| ResponseResolutionStep | Generate debt/affordability explanation. | 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.
Response Field Provenance
Where final response facts come from
| Response fact | Primary source | Stage where it enters context |
|---|---|---|
| creditRating | loan.credit.rating.check mapped payload | MCP_TOOL_RESULT #1 |
| fraudFlag | loan.credit.fraud.check mapped payload | MCP_TOOL_RESULT #2 |
| dti / availableCredit | loan.debt.credit.summary mapped payload | MCP_TOOL_RESULT #3 |
| applicationId / submitStatus | loan.application.submit mapped payload | MCP_TOOL_RESULT #4 (approved path) |
| Final decision sentence | planner answer + response derivation | MCP_FINAL_ANSWER + RESOLVE_RESPONSE_LLM_OUTPUT |
Notes on state and scope
ce_mcp_tool rows in example2 are scoped as:
intent_code = LOAN_APPLICATIONstate_code = ELIGIBILITY_GATE
This ensures tools are visible only in the loan decision state.
Troubleshooting
- Tool not resolved:
- verify
tool_codein DB matches handlertoolCode()exactly
- verify
- No tool call in planner path:
- check
McpPlannerprompt rows ince_mcp_planner - confirm active intent is
LOAN_APPLICATION
- check
- Timeouts/retries unexpected:
- inspect handler-specific
HttpApiExecutionPolicy - inspect mock delay values in
mockey/src/response/loan/*.json
- inspect handler-specific