MCP server · aperture 0.2.0 · 12 tools

The Aperture docs.

A safe way for AI agents to work in web systems that have no API. Every change is previewed, checked, read back, and undoable.

Open docs.md

Agents can also fetch /docs with Accept: text/markdown and get the same file.

What it is for.

Aperture is for changing records in a web admin portal or legacy business system that has no API: resident lists, user lists, customer or inventory screens, registrar pages. What it does that driving the page by hand does not: it turns the page into typed tools, shows what a change will affect before it happens (including records that go with it), reads the system back afterwards and compares it with the prediction, gives a receipt, and can undo. Reading is open: compile_site works on any public page without setup. Signing in and changing anything only happen on systems the operator listed, through the owner's authority. Start with find_systems (what you need, in plain words) or compile_site (a page address), then find to get exact keys, then delete or create with dry_run first. Compile every page whose records a change touches. Not for: systems that have a real API, sites you are not allowed to automate, or CAPTCHA and sign-in steps (a person does those).

Connect.

One endpoint, streamable HTTP: https://calm-steel-d31rh.run.mcp-use.com/mcp

Claude Code

claude mcp add --transport http "aperture" 'https://calm-steel-d31rh.run.mcp-use.com/mcp'

Any MCP client

{
  "mcpServers": {
    "aperture": {
      "type": "http",
      "url": "https://calm-steel-d31rh.run.mcp-use.com/mcp"
    }
  }
}

How a change works.

  1. 01

    Identify

    find returns unique keys. Names repeat, so every change takes a key, never a name.

  2. 02

    Preview

    A dry run shows what will change, including records that go with it (a resident's devices).

  3. 03

    Rules

    Owner limits apply. Inside them it commits; outside them it waits for the owner (awaiting_approval) or refuses with next moves.

  4. 04

    Commit

    The change runs once. A page that hangs is replaced after 17 s; the outcome is then read, never retried blindly.

  5. 05

    Read back

    The system is read again and compared with the prediction. The receipt says verified, and what was checked.

  6. 06

    Undo

    A committed change can be reversed from the saved copy. Fields that could not be put back are listed.

Rules for agents.

  • Act on keys from find, never on names.
  • Compile every page whose records a change touches (for example residents and devices), or the preview refuses with dependency_not_compiled.
  • Prefer apply and apply_batch: they preview, check the rules and commit in one call.
  • On awaiting_approval, wait with get_plan(wait_seconds). Do not retry a commit in a loop.
  • On policy_denied, call escalate once and continue with other items.
  • A receipt with not_yet_live means the change is recorded but not sent anywhere. Only the owner publishes.
  • Aperture never types a password and never passes a CAPTCHA. A person does those.

Tools.

12 tools. Names, parameters and types are taken from the live server; examples are from real eval runs on the GateAdmin32 replica.

ToolEffectPurpose
find_systemsReads onlyFind the systems you may work in, from a plain-words need.
get_instructionsReads onlyA short guide to the connected system: record kinds and rules. Read it once.
compile_siteReads onlyRead a page and turn its lists and forms into typed record kinds. Signs in with a stored login when needed.
findReads onlyLook up records and get their unique keys. Names are not unique; act on the key.
applyChanges recordsThe normal way to change one record: preview, rules, commit, read-back and receipt in one call.
apply_batchChanges recordsMany changes in one call, inside the rules. Dry-run shows the whole blast radius; canary checks the first few.
deleteChanges recordsDelete one record by key, in two steps: dry_run to preview, then plan_id to commit.
createChanges recordsCreate one record with a unique key, in two steps: dry_run, then plan_id.
undoChanges recordsReverse a committed change from the copy saved before it, and say what could not be put back.
get_planReads onlyWait for the owner's decision, or find out what really happened when an outcome is unknown.
escalateReads onlyAsk the owner for help when a rule blocks you. One message goes out; keep working on other items.
publishReaches outsideSend recorded changes to the outside world (for example, a gate controller). Always needs the owner. Cannot be undone.

find_systems

Reads only

Start here when you are not given an address. Describe what you need to do in plain words (for example 'remove a resident from the access list' or 'update a customer record'). Returns the systems you are allowed to work in that match, with the address to compile and any profile other agents shared. It never searches the open web.

needrequiredstring
What you need to do or find, in plain words.

Example

find_systems({ need: "remove a resident from the gate access system" })

{ "status": "found", "matches": [ { "name": "GateAdmin replica, residents (sign-in required)",
    "address": ".../Residents.aspx",
    "sign_in": "compile_site with login 'REPLICA', url '.../Login.aspx', then '.../Residents.aspx'" } ] }

get_instructions

Reads only

Start here. Returns a short guide to this system, the record kinds, and the rules. Read it once.

No parameters.

compile_site

Reads only

Point Aperture at a web page and it reads the page's forms, read-only, and turns each one into a typed record you can fill. Only works on hosts the operator has authorized. It never submits anything and never tries to get past a CAPTCHA: a person does that. Returns each form's id, fields (label, type, required, allowed values) and submit button.

urlstring
The page address. With login set, this is the sign-in page. Leave it out to read the current page again (after a submit, or when a page changed).
loginstring
Name of a login the operator stored on the server. You never see the password.
thenstring
With login: the page to compile after signing in.

Example

compile_site({ url: ".../Login.aspx", login: "REPLICA", then: ".../Residents.aspx" })

{ "status": "compiled", "tables": [ { "id": "residents", "key": "Dir", "rows": 91 },
    { "id": "vendors", "key": "Dir", "rows": 3 } ] }

find

Reads only

Look up records. Returns small typed items with their unique keys. Names are not unique: always act on the key. Free, nothing changes.

kindrequiredstring
Record kind, as compile_site or find_systems named it (for example residents or devices). Call compile_site first if you have none.
querystring
Text to match in any field, like a name. Leave empty to list.
limitinteger
Most items to return.

Example

find({ kind: "residents", query: "Dev Alder" })

{ "kind": "residents", "count": 2, "note": "Several matches. Names are not unique, use the key.",
  "items": [ { "key": "1004", "Resident": "Dev Alder", "Notes": "Unit 103D" },
             { "key": "1026", "Resident": "Dev Alder", "Notes": "Unit 125B" } ] }

apply

Changes records

The normal way to make a change. Previews inside the server. If the rules allow it, it commits at once and returns a receipt with a read-back. If a person must decide, it returns awaiting_approval with a plan_id to poll with get_plan.

oprequired"delete" | "create" | "update" | "publish"
One of delete, create, update, publish.
kindstring
Record kind, as compile_site or find_systems named it.
keystring
The unique key from find. Never a name.
recordobject
For create: the fields of the new record, including its key.
patchobject
For update: the fields to change, like { city: 'Leeds' }.

Example

apply({ op: "delete", kind: "residents", key: "1004" })

{ "status": "done", "receipt": "rcpt_7",
  "verified": { "ok": true, "subject_present": false, "matches_prediction": true,
                "counts_after": { "residents": 90, "devices": 166 } },
  "not_yet_live": "Recorded in this system. It also has a separate step ('Snd Data') that Aperture has not pressed...",
  "undo_until": "2026-10-19T10:10:57Z" }

apply_batch

Changes records

Apply many changes in one call. Everything inside the rules commits. Items that need the owner are listed with plan_ids. Refusals are listed with next moves. Stops at the first failed commit. Returns one summary, so you do not need many round trips.

opsobject[]
Up to 50 operations, each shaped like an apply call.
plan_idsstring[]
Instead of ops: commit plans the owner already approved, in order, with the first `canary` checked first.
stop_on_failureboolean
Stop at the first failed commit (the default).
dry_runboolean
Change nothing. Returns the total blast radius: everything that would be removed, dependents included.
canaryinteger
Commit only the first N, check they did exactly what was predicted, and halt the rest if any surprised or failed.

Example

apply_batch({ ops: [ { op: "delete", kind: "residents", key: "1001" }, ... ], dry_run: true })

{ "would_remove": { "residents": 91, "devices": 168 },
  "refused": [ { "key": "1006", "error": "policy_denied", "detail": "Daily delete limit reached: 5 of 5.",
                 "next": ["escalate(reason) to ask the owner for more"] } ] }

delete

Changes records

Delete one record by key. With dry_run: true it only previews: what is removed, the count change, which rule applies. Nothing changes. To commit, call again with the plan_id. For most jobs use apply, which does both when the rules allow.

kindrequiredstring
Record kind, as compile_site or find_systems named it (for example residents or devices). Call compile_site first if you have none.
keyrequiredstring
The unique key from find, like S014. Never a name.
dry_runboolean
Preview only: what would be removed, the count change, and which rule applies. Nothing changes.
plan_idstring
Commit a plan you already previewed.

Example

delete({ kind: "residents", key: "1004", dry_run: true })

{ "status": "previewed", "plan_id": "pln_23",
  "diff": ["- residents 1004", "- 2 devices",
           "(the devices above go with this residents automatically: do not delete them separately)"] }

create

Changes records

Create one record. The record needs a unique key. With dry_run: true it only previews. To commit, call again with the plan_id.

kindrequiredstring
Record kind, as compile_site or find_systems named it (for example residents or devices). Call compile_site first if you have none.
recordrequiredobject
Fields of the new record. Must include a unique key.
dry_runboolean
Preview only. Nothing changes.
plan_idstring
Commit a plan you already previewed.

undo

Changes records

Reverse a committed change using the copy saved before it. Reads the system back to verify. Works within the undo window. Published changes cannot be recalled.

receipt_idrequiredstring
The receipt id from the commit result.

Example

undo({ receipt_id: "rcpt_9" })

{ "status": "undone", "restored": true, "fields_not_restored": ["Phone"],
  "verified": { "ok": true, "counts_after": { "residents": 91, "devices": 168 } } }

get_plan

Reads only

Check a plan: awaiting_approval, approved, expired, committed. Use it to wait for the owner's decision instead of retrying. If the outcome of a change is unknown (the server stopped while it ran), this reads the system and says what really happened; it never runs the change again.

plan_idrequiredstring
The plan id from a preview, apply or apply_batch.
wait_secondsinteger
Wait up to this long for the owner to decide, so you do not burn turns polling.

Example

get_plan({ plan_id: "pln_23", wait_seconds: 10 })

{ "plan_id": "pln_23", "status": "committed", "diff": ["- residents 1020", "- 1 devices"] }

escalate

Reads only

Ask the owner for help when a rule blocks you, like the daily limit. One message goes out. Keep working on other items. Do not retry the blocked one.

reasonrequiredstring
One sentence on what you need and why.

Example

escalate({ reason: "Daily delete limit reached after 5 of 91; need owner approval to continue." })

{ "status": "pending_human", "ticket": "esc_2", "agent_can_continue_with": ["find", "create", "other deletes inside the limit"] }

publish

Reaches outside

Send staged changes to the outside world (for example to devices, a gate, or another system). It always needs the owner. Without plan_id it previews and asks the owner. It cannot be undone.

plan_idstring
Commit a send the owner has approved. Without it, publish previews and asks the owner.

Example

publish({})

{ "status": "refused", "error": "send_needs_owner",
  "detail": "3 changes are recorded but not sent. The send step is 'Snd Data' on Accounts.aspx; only the owner sends." }

Errors.

Every refusal returns error, detail and a next list of valid next moves.

errorMeaningNext move
not_authorizedA sign-in or change on a system the operator did not list, or an internal address.Read public pages freely; for changes ask the operator. Never look for a workaround.
human_requiredA CAPTCHA or similar needs a person.A person completes it, then call compile_site again.
no_stored_loginThe named login does not exist.Ask the operator to store it. Never type a password into chat.
not_foundNo record with that key.Use find and pass the key, not the name; see did_you_mean.
already_existsA create would duplicate a key.Use a different key or find the existing record.
dependency_not_compiledThe change affects a page that has not been compiled.compile_site that page (find_systems lists it), then try again.
awaiting_approvalThe owner has not approved this change.get_plan with wait_seconds. Do not retry the commit.
delegation_endedThe owner's delegation ended before the commit.Poll get_plan; it now waits for the owner.
changed_since_previewThe record changed after the preview.Run a new preview.
capability_changedThe page changed and Aperture could not prove a re-learned version.Nothing changed. Do not retry; tell the owner.
policy_deniedA cap such as deletes per day was reached.escalate with a reason.
send_needs_ownerRecorded changes wait for a send step only the owner can take.Tell the user the change is not live yet.
outcome_unknownThe server stopped while the change ran.get_plan reads the system. Never commit it again.
worker_busyThe browser was busy, or a page hung and was replaced.Retry a read after a few seconds; for a change, get_plan first.
undo_failedThe app refused to take the record back.The saved values are in the response; re-enter by hand and tell the owner.

A receipt can also say done_with_surprise (the system changed more than predicted; the extra items are listed), failed (the app refused or errored; the real state is reported), or carry fields_not_restored after an undo. verified.scope says which tables were compared; tables Aperture has not compiled are not checked.

Limits.

  • One browser per worker, so calls to one system run one at a time.
  • Product and search grids are read as read-only items; they cannot be changed.
  • Effects in tables Aperture never compiled are not checked (stated in every receipt's scope).
  • Sites with bot checks or CAPTCHAs need a person.

Hand these docs to your agent.

One click copies the whole page as markdown: connection, rules, every tool and every error.

Open docs.md