Call your MCP server the way an agent would.

Connect as one test identity, list the tools your server exposes, call them and assert what comes back. Reflow keeps each result with the run, so your agent can compare it with an earlier run.

reflow / MCP · billing serverRecorded output · fixture data
Captured billing flowLine 11 · selected step
  1. api.connect
  2. api.call tool="search_invoices" arguments="{\"query\":\"overdue\"}" expect="{\"textIncludes\":\"INV-1042\"}"
  3. api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"
  4. api.call tool="void_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"rpcError\":-32602}"
  5. api.disconnect
  6. # Run 2 repeats the same steps in a new session
  7. api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"

Exact recorded RFL. ‘invoice’ names the retained result in this presentation; it is not a runtime checkpoint. Comment lines explain retention and comparison. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
billing-fixtureStreamable HTTP · 2025-11-25
api.connect · HTTP 200
tools/list3 tools · 1 page
Senttools/list · params {}
  • get_invoicereadOnly trueschema 3797626b… · description b4f839da…
  • search_invoicesreadOnly trueschema 1dc63095… · description 27fafa01…
  • send_reminderdestructive false · idempotent false · readOnly falseschema 3797626b… · description 48814d33…
  • only 3 named toolspassed
  • noTrueAnnotations destructivepassed
Snapshot · invoice · beforeRun 1 · presentation label
search_invoicestools/call
Senttools/call · arguments {"query":"overdue"}
GotINV-1042 overdue 14 days INV-1043 overdue 3 days
  • textIncludes "INV-1042"passed
get_invoicetools/call
Senttools/call · arguments {"id":"INV-1042"}
Got{"id":"INV-1042","status":"overdue","amount_due":"120.00","currency":"USD"}
  • isError falsepassed
  • /status equals "overdue"passed
  • /amount_due type "string"passed
void_invoicetools/call · refusal expected
Senttools/call · arguments {"id":"INV-1042"}
Goterror -32602 · Unknown tool: void_invoice
  • rpcError -32602passed

api.disconnect · cleanup acknowledged (204)Run 1 · 7 of 7 expectations passed

Snapshot · invoice · afterRun 2 · presentation label
get_invoicesame steps, new session
Got{"id":"INV-1042","status":"overdue","amount_due":"0.00","currency":"USD"}
  • isError falsepassed
  • /status equals "overdue"passed
  • /amount_due type "string"passed

All recorded steps repeated, including the expected refusal and disconnect.Run 2 · 7 of 7 expectations passed

The retained invoice still says overdue. The amount due is now 0.00; passing expectations alone do not establish that this change is intended.

Delta · invoiceBefore · Run 1 → After · Run 2
stepBeforeAftercompared
tools/list 2 of 2 passed 2 of 2 passed match
search_invoices 1 of 1 passed 1 of 1 passed match
get_invoice 3 of 3 passed 3 of 3 passed differs
void_invoice 1 of 1 passed 1 of 1 passed match
Review point · get_invoiceretained text
Before · Run 1{"id":"INV-1042","status":"overdue","amount_due":"120.00","currency":"USD"}
After · Run 2{"id":"INV-1042","status":"overdue","amount_due":"0.00","currency":"USD"}

Every expectation passed in both runs, yet the retained get_invoice results differ. Highlighted: the field that changed.

Agent review · exampleINV-1042 is still overdue, but its amount due now reads 0.00. Was that change intended?

Recorded MCP plugin output from two runs against a synthetic billing server on localhost. “Sent” is the request the server received; replies, checks and comparisons are the plugin’s retained snapshots. Fixture data; layout illustrative. “invoice” is a presentation label for the retained get_invoice result, not a named runtime checkpoint.

Recorded snapshots for get_invoice
api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"

// Run 1 · succeeded
{
  "contentTypes": [
    "text"
  ],
  "failures": [],
  "isError": false,
  "jsonAssertions": [
    {
      "index": 0,
      "operator": "equals",
      "passed": true,
      "pointer": "/status"
    },
    {
      "index": 1,
      "operator": "type",
      "passed": true,
      "pointer": "/amount_due"
    }
  ],
  "kind": "result",
  "redacted": false,
  "structuredContentSha256": null,
  "text": "{\"id\":\"INV-1042\",\"status\":\"overdue\",\"amount_due\":\"120.00\",\"currency\":\"USD\"}",
  "textTruncated": false,
  "tool": "get_invoice"
}

// Run 2 · succeeded
{
  "contentTypes": [
    "text"
  ],
  "failures": [],
  "isError": false,
  "jsonAssertions": [
    {
      "index": 0,
      "operator": "equals",
      "passed": true,
      "pointer": "/status"
    },
    {
      "index": 1,
      "operator": "type",
      "passed": true,
      "pointer": "/amount_due"
    }
  ],
  "kind": "result",
  "redacted": false,
  "structuredContentSha256": null,
  "text": "{\"id\":\"INV-1042\",\"status\":\"overdue\",\"amount_due\":\"0.00\",\"currency\":\"USD\"}",
  "textTruncated": false,
  "tool": "get_invoice"
}

// Comparison
MCP observations differ.

Check what one identity can see and call.

A flow connects over Streamable HTTP, reads the complete tool inventory and calls tools with the arguments you choose. Each step asserts a result, a tool error or the server’s JSON-RPC refusal, and keeps the redacted text it observed.

  • Tool inventories with annotation hints and schema hashes
  • Redacted call results and JSON assertion outcomes
  • JSON-RPC refusals and session cleanup status

Compare tool results across runs.

Reflow compares tool inventories by name, annotation hints and schema and description hashes, and reports whether each retained call result matches the earlier run. Every expectation can pass in both runs while a result still changes.

In the example, get_invoice still reports INV-1042 as overdue, but the amount due has changed from 120.00 to 0.00. Your agent can bring both retained results into the PR review and ask whether that change was intended.

Bring the evidence into PR review

Tools, calls and a refusal in one flow

Keep the endpoint and the test identity’s credential in the private MCP binding, and select the endpoint as the target so its origin is admitted. Point the flow only at a test server: api.call really calls the tool.

Your agent can inspect the installed plugin’s commands and write the flow using your application knowledge. Select the plugin version in the flow’s requirements.

Flow excerptMCP
---
providers:
  api: MCP
---

api.connect
api.tools expect="{\"only\":[\"get_invoice\",\"search_invoices\",\"send_reminder\"],\"noTrueAnnotations\":[\"destructive\"]}"
api.call tool="search_invoices" arguments="{\"query\":\"overdue\"}" expect="{\"textIncludes\":\"INV-1042\"}"
api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"
api.call tool="void_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"rpcError\":-32602}"
api.disconnect

Plugin requirements and private connection settings are configured separately.

Check the rest
of the application.