Public leaderboard

Public assessment

homespunapps/homespun (@homespunapps/mcp)

homespunapps-mcp · v1.6.42 · scanned

What changed in the harness

Selection accuracy 98→98, token cost up 2%, unconfirmed writes 0%→0%.

Category breakdown

Where the score comes from.

Earned points across the four signals Gradable measures. Safety and Legibility are scored out of 30; Economics and Discoverability are scored out of 20.

01Safety

0.0 / 30

0.0 out of 30
02Legibility

25.9 / 30

25.9 out of 30
03Economics

10.0 / 20

10.0 out of 20
04Discoverability

10.1 / 20

10.1 out of 20

Highest-impact fix

Estimated gain +30 points

Add explicit identity and permission preflight tools

Expose machine-readable principal/tenant confirmation and a non-mutating permission check so agents can verify both before destructive actions.

Description evidence

Defects and rewrites.

11 defects found across the exposed tool descriptions. Suggested rewrites make purpose, inputs, boundaries, and returns easier for an agent to understand.

Tool Defect types Suggested rewrite
list_rows
params_unexplained
List rows in a v2 app's mutable collection, for the app given by `app_id` (the v2 app's id, as returned by the deploy_app create response) and `collection` (the collection's name as declared in that app's manifest). This is also how a collection's current state is polled, since MCP has no streaming: pass the prior next_cursor as `since` to fetch only rows that are new or changed. Returns { rows, next_cursor, has_more }.
get_row
params_unexplained
Fetch a single row by its key from a v2 app collection, for the app given by `app_id` (the v2 app's id, as returned by the deploy_app create response) and `collection` (the collection's name as declared in that app's manifest), through a dedicated relay route rather than a client-side scan. Returns { row }, or an isError row_not_found.
upsert_row
params_unexplained
Create a row in the v2 app given by `app_id` (its id, as returned by the deploy_app create response), in the collection named by `collection` (declared in that app's manifest), or return the existing row when `key` is already present (deduped:true). Row creation goes through this tool; there is no separate strict-create verb. Omit `key` to add a new row with a server-generated key, or pass `key` to ensure a row exists at that key. The collection must be declared in the app's manifest with 'agent' in its `write` list, which is the list that gates creates. When `key` matches a row the collection's `read` list does not reach for this caller, the result is row_not_found rather than the row, matching what get_row would return, so this never reads past `read`. Returns { row, deduped? }.
update_row
params_unexplained
Update an existing row in the v2 app given by `app_id` (its id, as returned by the deploy_app create response), in the collection named by `collection` (declared in that app's manifest), replacing its data. Gated by the collection's `update` role list when it declares one, and by its `write` list otherwise, so a collection that scopes updates to the row's `creator` refuses an edit on someone else's row. Pass if_match with the row's current version for an optimistic-locked update; on a version mismatch the relay returns the current row, which is what a retry needs. Returns { row }.
delete_row
params_unexplained
Soft-delete a row from a collection in the v2 app given by `app_id` (its id, as returned by the deploy_app create response) and `collection` (the collection's name as declared in that app's manifest). RECOVERABLE: the row is tombstoned, not destroyed, and restore_row brings it back for 30 days (see list_deleted_rows). A watcher sees the deletion live as op:delete on the change feed. Pass if_match for an optimistic-locked delete. Returns { deleted: true }.
list_deleted_rows
params_unexplained
List a collection's recently deleted rows (the recovery bin) in the v2 app given by `app_id` (its id, as returned by the deploy_app create response) and `collection` (the collection's name as declared in that app's manifest). Deleting a row is a SOFT delete, so it can be restored with restore_row until recoverable_until passes (30 days after deletion by default). Owner or agent only, and deliberately independent of the collection's read permissions. Rows already purged appear with purged:true and cannot be restored. Returns { rows, next_before }.
restore_row
params_unexplained
Restore a soft-deleted row, undoing delete_row, in the v2 app given by `app_id` (its id, as returned by the deploy_app create response) and `collection` (the collection's name as declared in that app's manifest). The row comes back with its original data and creator, its version bumped. Find restorable keys with list_deleted_rows. Owner or agent only. Fails with restore_expired if the row was purged, or restore_conflict if another live row took a unique value this one held while it was deleted. Returns { row }.
get_feed_events
params_unexplained
Poll the v2 app change feed for the app given by `app_id` (its id, as returned by the deploy_app create response): what has happened - row creates, updates and deletes, from any writer, agent or human. It is the long-poll analogue of `homespun apps watch`, since MCP has no streaming. The loop is: call with no `since` first, process the returned entries, keep the cursor, then call again passing it as `since` to get only newer entries. Passing wait (around 25) holds the request open until an entry arrives or it times out, which is how the feed is waited on rather than busy-polled. A `since` older than the retention floor returns resync_required, and the collections are then re-listed with list_rows. Returns { entries, cursor, truncated }.
members
params_unexplained
A v2 app's membership (auth spec section 6): who besides the owner can sign in to a private app and write to member-scoped collections, for the app given by `app_id` (the v2 app's id, as returned by the deploy_app create response). Actions: add invites or attaches a member by email, attaching immediately when the email already has a Human and otherwise sending a magic-link invite; list returns the app's owner and members; set_role changes an existing member's declared custom role in place, or clears it when null, and leaves their sessions intact, which is what makes it the way to re-role someone rather than removing and re-adding them; remove is idempotent and also revokes the human's live sessions on this app, and the app owner cannot be removed; roles returns the derived roles summary, giving the effective access a holder actually has per declared role and collection, reported separately for signed-in members and for grant-link holders because their role floors differ, along with member and active-grant-link counts.
grants
params_unexplained
A v2 app's grant links (M5), for the app given by `app_id` (the v2 app's id, as returned by the deploy_app create response). A grant link is a capability URL that confers a declared custom role (x-homespun-manifest.roles) on a stable per-holder anonymous identity, so a holder's own rows are isolated by author/:own scoping. A grant does not escalate to owner, member or agent. Actions: mint creates a link and returns a `grant_url` carrying the token in its #g= fragment, shown once and not recoverable afterwards; list returns the app's links and never a token; revoke is idempotent. mode 'once' is one-time, claimed by the first browser to open it; 'multi' is shared, capped by max_uses within expiry. An optional pin (pin_row_key or pin_where) narrows a holder to specific rows and never widens their access. One consequence worth knowing when minting: a write-only grant pinned to a single row key can still read that row's existing data back through create dedup, so such a grant exposes that row's current contents to the holder.
ingest
params_unexplained
A v2 app's inbound catch-hooks (inbound-webhooks), for the app given by `app_id` (the v2 app's id, as returned by the deploy_app create response). A catch-hook lets an external system such as Stripe, Zapier, Make, Home Assistant or an email router POST JSON to a secret URL that writes into a declared collection, so the app receives data with no agent online. Hooks are declared in the manifest (x-homespun-manifest.ingest) and materialized at deploy, so this tool has no create or delete: it reads back the URL, rotates a leaked one, and manages the opt-in signing secret. After deploying a manifest that declares a hook, list is what yields the exact URL to paste into the external system. Actions: list returns the app's hooks, each with its full secret URL, current rule collection, mode, wake and handshake settings, per-status delivery counts and signing-secret state; rotate mints a fresh URL secret for one hook by name and returns the new url once, after which the old url stops working immediately with no redeploy needed; set_signing_secret provisions or rotates a hook's signing secret, which is a different secret from the URL and is what a provider HMACs the body with, minting one returned once when `secret` is omitted or storing a provider value verbatim when it is passed, and never echoing it back; clear_signing_secret removes it. Signature verification currently ships dark: nothing verifies a signature yet.

Selection evidence

Confusable tool pairs.

4 pairs where similar names or overlapping descriptions may send an agent toward the wrong tool.

Tool A Tool B Confidence Why they collide
list_rows list_deleted_rows medium Names differ by a single word and share the same collection vocabulary; a task like 'list the rows in collection X' is ambiguous about live rows vs the recovery bin, so an agent may call list_rows when list_deleted_rows was intended, especially if the user says 'show the rows, including ones I deleted'.
upsert_row update_row medium Both write data at a key with nearly identical schemas (app_id, collection, key, data); a task like 'set the data for row X' or 'make sure row X holds this value' is genuinely ambiguous between create-or-return (upsert) and replace-existing (update), particularly when row existence is unknown.
upsert_row restore_row medium Both can make a row exist at a key; a task like 'bring row X back' or 'recreate the data at key X' could route to upsert_row (fresh create/return) when the correct action is restore_row (resurrecting the soft-deleted row with original data and creator), and a terse task does not disambiguate resurrect vs write.
get_row restore_row low For a user asking to 'get/see/recover the data of row X' after it was deleted, the word 'get' pulls toward get_row (which returns row_not_found for tombstoned rows) while the situation calls for restore_row; plausible, but the descriptions are explicit enough that most agents resolve it.

Compare the field

One score is useful.
The evidence makes it actionable.

Back to the leaderboard