Remote MCP Server Tool-Surface and Permission Change Ledger
Pricing
from $10.00 / 1,000 run_starts
Remote MCP Server Tool-Surface and Permission Change Ledger
Watch remote MCP servers for tool-surface and permission changes. The Actor reads the declared tool list with anonymous requests, compares it against a stored digest, and reports each added tool, removed tool, changed input schema, changed destructive-act
Pricing
from $10.00 / 1,000 run_starts
Rating
0.0
(0)
Developer
kingii98
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Watch the remote MCP servers that you connect your agents to, and learn the day a server adds a tool, changes a tool input schema, changes a destructive-action hint, or starts to demand authentication. Each of those changes alters what your agent can do, so each of them is a security event, not a parse break.
The Actor answers one question for the platform owner who connects agents to third-party MCP servers: what can my agents do today that they could not do yesterday?
The Actor sends anonymous requests only. It accepts no credential, it uses no
browser and no proxy, and it calls no tool. It reads the declared surface with the
MCP Streamable HTTP transport: one initialize call, one initialized
notification, one tools/list walk, and — when the server declares those
capabilities — one resources/list walk and one prompts/list walk. A server that
demands OAuth gets up to two unauthenticated .well-known GET requests of RFC 9728,
so that the row reports where the authorization lives.
The ledger
The Actor stores one digest of each server's tool surface in the named key-value
store mcp-tool-surface-ledger, under the record key that ledger_name names. The
named store outlives the run; the run's own default store does not.
- First run for a server: the Actor writes the baseline and records no change.
The row carries
ledger_status: "created". - Later run: the Actor compares the fresh digest against the stored one. When
the digests differ, it walks the two surfaces field by field and writes one change
row for each difference. The row carries
ledger_status: "compared". - A server it could not read: the stored entry is kept unchanged. A stale
baseline is better than one that records an error page. The row carries
ledger_status: "unavailable".
The record is written once, at the end of the run, so an aborted run leaves the previous ledger intact. Servers that this run did not read keep their entry, and the record holds at most 500 servers.
Change classes
| Change class | What it means for a connected agent |
|---|---|
tool_added | The server offers an action that your agent did not have. |
tool_removed | An action your agent relies on is gone. |
input_schema_changed | The arguments of a tool changed. A stored prompt or a stored call may now be wrong. |
destructive_hint_changed | A tool now declares (or stops declaring) that it destroys data. |
auth_requirement_changed | The server started or stopped demanding authentication, or stopped allowing the anonymous listing. |
read_only_hint_changed | A tool lost or gained its read-only promise. |
open_world_hint_changed | A tool changed whether it reaches outside the server. |
tool_description_changed | The text that steers your agent's tool choice changed. |
server_version_changed | The server reports a new version. |
The first five are the default. Each recorded change is one billed event, so a class that you do not select costs nothing.
Severity. An authentication change is high. A destructive hint that turns on,
or that stops being declared, is high; one that turns off is medium. A tool that
loses a readOnlyHint: true is high. An added tool, a removed tool, a changed
input schema and a changed open-world hint are medium. Everything else is low.
A server that stops the anonymous listing hides every tool at once. That is one authentication event, not one removal for each tool, so the tool diff is suppressed for that run and resumes when the listing is readable again.
Input
| Field | Type | Default | Meaning |
|---|---|---|---|
servers | array of 1 to 200 URLs | the four public demo servers | Remote MCP endpoint URLs that speak the Streamable HTTP transport. |
ledger_name | string | DEFAULT | The record key of the stored baseline set inside mcp-tool-surface-ledger. Use one name for each connected server fleet. |
alert_on | array of change classes | the five classes above | Which change classes become a change row. |
request_timeout_seconds | integer 2 to 120 | 20 | Per-request timeout for each MCP call and each metadata request. |
concurrency | integer 1 to 20 | 5 | How many servers are read at the same time. |
max_redirects | integer 0 to 10 | 3 | Redirect hops for each request. Every hop is resolved and checked again. |
max_response_bytes | integer 10000 to 5000000 | 2000000 | Per-response body cap. |
count_resources_and_prompts | boolean | true | Also call resources/list and prompts/list when the server declares those capabilities. |
probe_oauth_metadata | boolean | true | Send up to two RFC 9728 .well-known GET requests for a server that demands authentication. |
max_server_seconds | integer 5 to 600 | 60 | Wall-clock budget for one server. |
max_run_seconds | integer 30 to 3600 | 900 | Wall-clock deadline for the whole server list. |
Every field has a schema default, so a run with the empty input {} reads the four
public demo servers and succeeds.
Safety limits
- Only
httpandhttpsURLs, and no credential in a URL. localhost, and any hostname that resolves to a private or reserved address, is refused. Every redirect hop is resolved and checked again before it is followed, and a chain that repeats a URL is stopped.- An entry that is not a usable target — a malformed URL, a foreign scheme, a
localhosttarget, a URL with a credential, or a repeat of an earlier URL — becomes one row with theBLOCKED_TARGETreason code. It never stops the run, and it is never charged. - At most 200 servers, 500 tools for each server, 5 cursor pages for each list method, and 5 MB for each response body. Each response body is read by a streaming reader that stops at the cap.
- The request timeout does not bound one server on its own, because one server takes
up to 17 requests and each request may take several redirect hops. The worst-case
wall clock of a run is therefore . With the default fixture that is 55 seconds, well inside the platform's five-minute window.min(ceil(servers / concurrency) * max_server_seconds, max_run_seconds)
Output
The dataset holds one summary row, one row for each server, and one row for each
recorded change. The record_type field separates them.
Server row (record_type: "server"): server_url, final_url, ok,
reason_code, error, http_status, transport, protocol_version,
server_name, server_version, anonymous_listing_allowed, auth_required,
oauth_protected_resource_metadata_url, oauth_authorization_servers,
tools_total, resources_total, prompts_total, tool_digest,
previous_tool_digest, digest_changed, tools[] (each with name,
description_hash, input_schema_hash, read_only_hint, destructive_hint,
open_world_hint), tools_truncated, ledger_status, change_count,
first_seen, last_changed, digest_algorithm, bytes_read, response_time_ms
and observed_at.
Change row (record_type: "change"): server_url, change_class, tool_name,
field, old_value_hash, new_value_hash, old_value, new_value, severity
and observed_at.
Summary row (record_type: "summary"): servers_requested, servers_checked,
servers_changed, servers_unreachable, servers_requiring_auth,
servers_allowing_anonymous_listing, anonymous_listing_share,
baselines_created, servers_compared, changes_total, changes_by_class,
changes_by_severity, alert_on, ledger_store, ledger_name,
digest_algorithm and observed_at.
Reason codes
OK, AUTH_REQUIRED, TRANSPORT_LEGACY_SSE, TRANSPORT_UNSUPPORTED,
HTTP_ERROR, RPC_ERROR, NOT_JSON, RESPONSE_TOO_LARGE, TOOLS_LIST_FAILED,
TIMEOUT, DNS_FAIL, CONNECT_FAIL, BLOCKED_TARGET, REDIRECT_LOOP and
RUN_DEADLINE.
A changed surface, a server that demands OAuth, an unusable target and an unreachable server are all business verdicts: the run reports them in the dataset and in the status message, and the run itself succeeds.
Pricing: pay per event
| Event | Unit | Counted as |
|---|---|---|
run_start | one for each run | Charged once, before any server work. It pays for the container start and the summary row. |
server_checked | one for each server contacted and compared against the ledger | Charged once for each server whose ledger_status is created or compared. A server that could not be reached, and an input entry that is not a usable target, were neither contacted nor compared, so neither is charged. |
surface_change_recorded | one for each recorded change of a tool or permission field | Charged once for each change row, that is once for each change_count of every server row. |
Before it reads a single server, the run limits the server list to the number of
server_checked charges that its maximum total charge still allows. The servers
that fall outside that budget are not read, and they keep their stored ledger entry
unchanged for the next run.
Repeat use
Tool surfaces change with each vendor release. Run the ledger each day or each week over the whole connected server list, and again before each agent release. The value is the cross-run baseline: the first run creates it, and every later run turns it into a list of events that you can act on.
Local development
uv syncuv run pytestuv run ruff check .