When your user is an agent

Drew Post··10 min read
synthetic-monitoringmcpai-infrastructureperspective

A humanoid service robot holding a tablet.

Every synthetic check encodes an assumption about who is on the other end. An uptime ping assumes a person waiting for a page to load. A scripted browser transaction assumes a person clicking log in, then add to cart, then pay, at human speed, in a real browser rendering real pixels. Even the industry's language gives it away: we call it a "user journey," and the user we mean, without ever quite saying so, is a person.

That assumption held for thirty years because it was true. The traffic that mattered came from people with browsers, so a tool that simulated one of those people well could tell you whether your product worked.

That's no longer the whole picture. I've written before about how the category kept answering last decade's question while the ground moved underneath it. This post is about one specific shift: the persona itself.

The classic persona, a person driving a browser through a rendered journey, next to four software callers and the check that covers each: REST APIs by HTTP with openapi_conformance, MCP servers by an MCP monitor that hashes inputSchema but not descriptions, docs by a plain GET with body_contains, and agent-driven UIs only partly by a scripted browser check.

A growing share of your traffic isn't human

A coding agent in a CI pipeline calls your endpoints to run an integration test it wrote itself. A support assistant calls an MCP server you stood up so it can look up an order without an engineer wiring a bespoke integration. A research agent fetches your docs to work out how your product behaves before it ever asks a person a clarifying question. In some products a browser-use agent is now driving the UI directly on a person's behalf, clicking the same buttons a human would click, except nothing about how it decides what to click is human.

These callers aren't edge cases. They're becoming a normal, load-bearing part of how a product gets used, the same shift from the other direction I've written about with coding agents. A synthetic monitor built entirely around the human persona says nothing about whether any of it works.

Where "it works" and "it works for an agent" diverge

Agents fail differently from people, and the differences don't show up in a human-shaped check.

A schema change invisible to a person breaks tool calling

Rename a JSON field from customer_id to customerId, or make a previously optional parameter required, and a person skimming a rendered page never notices. An agent parsing the response by exact key either throws or, worse, silently drops the field and improvises a plausible replacement. Your uptime check still reports 200.

A rewritten tool description misleads the model

In MCP, the natural-language description attached to a tool is part of the contract. It's the text the model reads to decide which tool to call and what arguments to pass. Rewrite that description and the model's behaviour can shift while the schema underneath stays put. That's a real gap even in dedicated MCP monitoring: schema-drift detection that hashes a tool's inputSchema won't catch a description-only edit, because the hash is over the schema, not the prose. Whatever your monitoring catches, treat a tool description edit as a reviewed API change.

Auth built for a person doesn't work for a machine

An interactive OAuth redirect, a magic link, an SMS code: all of them assume a browser and a human working through a consent screen. An agent running on a schedule, or granted a service credential by another system, needs a client-credentials flow or a long-lived key. A sign-in-with-Google button is no use to it. Migrate your auth without keeping a machine path and you break every agent integration silently, because nobody's staring at a login page to notice.

Rate limits tuned for browsing don't survive agent loops

A person reloads a page every few seconds at most. An agent that gets a failed tool call can retry with adjusted arguments in a tight loop as part of finishing one task. Limits set for human traffic either throttle a legitimate agent into failure or get raised so far they stop protecting anything.

Latency compounds across a chain

A single endpoint's P95 hides this. An 800ms response is fine for a person waiting on one page load. An agent that chains eight tool calls to finish one task pays that 800ms eight times, plus model inference between each step. A perfectly healthy per-endpoint SLO can sit underneath a task that times out from the agent's side, and nothing in the individual check tells you that.

A schematic timeline: a person waits 0.8 seconds for one page, an agent chaining eight 800 ms tool calls with inference between them takes about 10.6 seconds, and one failed call plus a retry pushes the task past an example timeout, with inference gaps and the timeout marked as illustrative.

Docs that render for a person can be invisible to a fetch

A documentation site that needs JavaScript to paint any body text, or hides the answer behind a client-side search widget, works fine for a person with a browser tab open. An agent issuing a plain HTTP GET against that same URL can get back an empty shell and nothing else, and it either hallucinates an answer or gives up.

None of these shows up as a failed HTTP check, yet each one is a broken product for a user that happens to be software.

A matrix of the six failure modes showing that a person notices none of them while an agent breaks, is misled, throttled or timed out by each, alongside what catches it: contract and schema-hash checks for field changes, a manual diff review for tool descriptions, a machine-credential check for auth, a plain GET with body_contains for docs, and inventory work for rate limits and chain latency.

Widen the persona

Bolting a new check type onto the old ones doesn't fix this. The synthetic persona has to widen from "a person with a browser" to "whatever kind of client actually depends on this surface," and each surface gets monitored at the level its caller operates at.

For a human, that level is a rendered page. For an agent calling an API, it's the contract: does the response still match the shape the client was built against? For an agent talking to an MCP server, it's the protocol session: did the handshake complete, is the tool the agent needs still in the catalog, has its schema moved since the last time anything checked. For an agent reading your docs, it's whatever a bare fetch actually returns.

I build Yorker, so weigh what follows accordingly. The argument doesn't need the product, though. If a meaningful share of the traffic that matters to you is software, the surfaces it depends on belong in the same SLOs as your human-facing ones, checked at the protocol level those callers use rather than pinged for a status code.

A monitor that speaks the same protocol your agents do

In practice that means checking API contracts and MCP sessions the way a real client exercises them. In yorker.config.yaml, that's an HTTP monitor with an openapi_conformance assertion next to an MCP monitor:

# yorker.config.yaml
project: my-api

monitors:
  - name: "Orders API (agent-facing)"
    type: http
    url: https://api.example.com/v1/orders
    frequency: 1m
    locations:
      - loc_us_east
      - loc_eu_central
    assertions:
      - type: status_code
        value: 200
      - type: openapi_conformance
        specId: spec_orders_v1
        validateHeaders: true

  - name: "Support MCP server"
    type: mcp
    endpoint: https://mcp.example.com/mcp
    frequency: 5m
    locations:
      - loc_us_east
      - loc_eu_west
    timeoutMs: 30000
    auth:
      type: bearer
      token: "{{secrets.MCP_PROBE_TOKEN}}"
    expectedTools:
      - get_order_status
      - create_return
    testCalls:
      - toolName: get_order_status
        arguments:
          order_id: "test-probe-001"
        expectedOutputContains: "probe"
    detectSchemaDrift: true

The HTTP monitor checks more than the 200. openapi_conformance validates the live response's status code and body shape against a spec you've registered in Yorker, so a field rename or a newly required parameter fails the check on the run it ships, not the week an agent starts sending malformed requests because of it.

The MCP monitor runs the actual session: initialize, notifications/initialized, tools/list, then the testCalls you've defined, and it hashes every tool's input schema to catch drift between runs. I've written the full mechanics of how that works separately, so I won't repeat it here.

The Tools tab of an MCP monitor in Yorker: the discovered tools with their input-schema hashes, and a schema-drift panel reporting no changes this run.

What this doesn't do

Both of those checks are deterministic. They run the same JSON-RPC session or the same HTTP request every cycle and assert against a spec or an expected shape you defined. Neither one runs an LLM to decide what to do next, and the argument above shouldn't be read as implying otherwise.

An MCP monitor won't catch a tool description edit that leaves the schema untouched, for the reason above: drift detection hashes inputSchema, not description. If your tool descriptions change, review that diff the way you'd review any other API contract change. A browser check, similarly, runs a Playwright script, hand-written or generated from a plain-language description, that exercises specific steps in a specific order. It tells you whether the flow it was built against still works. It won't tell you whether a browser-use agent, reasoning fresh about a redesigned page, could still work out where to click. Those are different questions, and only one of them has a good deterministic answer today.

We hold ourselves to the docs-for-a-fetch standard too. Every page on the Yorker site, including this one, has a plain Markdown twin at the same URL with .md appended, and /llms.txt indexes the lot, so an agent doing a GET against our docs gets the same content a person gets from the rendered page.

Your agent-facing surface: an inventory

Whatever you monitor it with, start by knowing what you have:

  • Every MCP server you run in production: endpoint, auth type, tool count, and whether anything is watching for schema drift.
  • Every API a coding agent, workflow tool, or third-party integration calls without a human in the loop, and whether that path is checked against a contract or just a status code.
  • Whether auth on those paths is machine-shaped (API key, service account, client credentials) or quietly assumes an interactive browser session.
  • Your rate limits, checked against agent retry behaviour rather than human browsing patterns. A tight retry loop from one client doesn't look like a person refreshing a page.
  • Whether your published docs, and any llms.txt you've shipped, return usable text to a bare fetch, not only to a browser with JavaScript enabled.
  • A rough latency budget for the whole chain an agent runs rather than the slowest single call inside it, if you can estimate how many calls a typical task makes.
  • Which of the above already sits inside an SLO next to your human-facing app, and which one currently has nobody watching it at all.

That last one is usually the honest answer to "are we monitoring our agent-facing surface": mostly not, because nobody built the persona for it yet.

Start free, no credit card required →