Homespun
by homespun.dev · remote · local (npm) · no sign-in to list tools · v1.6.98 · published by dev.homespun
Deploy a multi-user web app from your agent: hosting, auth, database, and permissions.
27 tools: 16 can delete or cancel things, 1 do something that can't be undone, 5 change things and 5 only read.
Scanned Oct 21, 2026
- Deletes or cancels16Claude Code: deny: see these tools
- Can't be undone1Claude Code: ask: see these tools
- Changes things5Claude Code: ask: see these tools
- Only reads5Claude Code: allow: see these tools
23 checks matched something in the tool definitions. No finding is not a safety guarantee. How the checks work.
claude mcp add --transport http homespun https://homespun.dev/mcpFindings
Matches of fixed checks, with the text they matched.
The server marks "list_deleted_rows" read-only (readOnlyHint: true), and its own name, description or input give the class "irreversible".
What this check looks forMatched text
annotations: {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}; The name "list_deleted_rows" says "list": a word for reading.
- Infodocument_upload
The description mentions "credential".
What this check looks forMatched text
description: "...he declared bytes and is not an account credential. The returned `document_id` is used by..."
- Infodeploy_app
The description is 4,505 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...bility, created } on create, or { app_id, version, compat, breaks? } on redeploy."
- Infoapps
The description is 2,528 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...unbinds one domain, or all of them when no `domain` is given, and is idempotent."
- Infocredentials
The title mentions "Credentials".
What this check looks forMatched text
title: "Manage App Service Credentials"
- Infocredentials
The annotations title mentions "Credentials".
What this check looks forMatched text
annotations.title: "Manage App Service Credentials"
Show 17 more findingsShow fewer
- Infocredentials
The description mentions "credentials".
What this check looks forMatched text
description: "A v2 app's scoped service credentials (#1354, #1355): the bearer token an app..."
- Infocredentials
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "mint: create a scoped service credential, the bearer token an app owner points a..."
- Infocredentials
The input schema at properties.mode.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.mode.description: "...an unnamed collection is denied, so the credential can never reach anything it was not han..."
- Infocredentials
The input schema at properties.grants.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.grants.description: "...which of read/create/update/delete this credential may attempt there (an entry may name ze..."
- Infocredentials
The input schema at properties.grants.items.properties.scope.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.grants.items.properties.scope.description: "...ows every row-addressed op to rows this credential itself wrote last. Inert for create."
- Infocredentials
The input schema at properties.members.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.members.description: "...pp's member directory appearing in this credential's boot/hello payloads. Defaults to fals..."
- Infocredentials
The input schema at properties.label.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.label.description: "...ptional owner-facing label shown in the credential list."
- Infocredentials
The input schema at properties.credential_id.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.credential_id.description: "pause / resume / rotate / revoke. The credential id (see list's `id` field)."
- Infoconnections
The description mentions "credential".
What this check looks forMatched text
description: "A v2 app's Connections: the stored credential (a static header token, or a full gener..."
- Infoconnections
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "...e: store a webhook connection, a stored credential (static header token or a full generic..."
- Infoconnections
The input schema at properties.allowed_host.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.allowed_host.description: "...wildcard ("*.zohoapis.com"). The stored credential is attached to a delivery only when its..."
- Infoconnections
The input schema at properties.header_name.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.header_name.description: "create only (static). The header the credential rides in. Defaults to "Authorization"."
- Infoattachments
The description is 2,115 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...capability URL, shown once, that a browser can GET without the caller's API key."
- Infokey
The description mentions "credential".
What this check looks forMatched text
description: "...hands a CLI or child process a working credential, and the raw value is not retrievable a..."
- Infokey
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "...hands a CLI or child process a working credential; the sibling is a distinct key that sho..."
- Infofeedback
The description is 2,126 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...; list returns this agent's own submissions, newest first, paginated by `before`."
- Infocommunity
The description is 2,313 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...ved version; reject takes a required note that lands in the publisher's app feed."
Tools
Open a tool for its reasons and full description. Descriptions are the publisher's own words, shown as they are.
27 tools
Deletes or cancels 16Claude Code: deny
agentAgent identity and binding.No findings
Why it is in this group
- The input takes a statement or command to run (input.code), which can change anything it can reach.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it Agent identity and binding. Actions: whoami returns the resolved relay URL, the active profile and whether a key is configured, with no network call and no secrets; claim binds this agent to a human using a one-shot claim code from their Settings UI, and is one-way; logout clears the locally saved key and profile but does not revoke it on the relay, which is what the `key` tool's revoke action does.
Annotations the server declares
- title: Manage Agent Identity
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
appsThe v2 app lifecycle apart from creation and redeploy, which deploy_app covers.1 finding
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The description is 2,528 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...unbinds one domain, or all of them when no `domain` is given, and is idempotent."
Description, as the server wrote it The v2 app lifecycle apart from creation and redeploy, which deploy_app covers. Actions: list returns the owning human's apps; show returns full detail including manifest, timezone and has_share_token; audit is a read-only security review of every app the caller owns, computed from each app's stored manifest, which is what makes it see apps that were deployed once and never redeployed (a deploy-time warning never reaches those). It reports collections whose declared permissions expose them, worst first: severity 'high' means an anonymous visitor can exploit it today, typically a collection that admits "anyone" to write with no separate 'update' list, so any visitor can overwrite rows other people created rather than only adding their own. It changes nothing; the fix is a redeploy declaring the missing list, and the right list differs per app, so read the app before proposing one. update changes visibility and timezone, the slug being immutable, and switching to 'link' returns a share_url once; share_link_rotate issues a new share token for a 'link' app, returning a new share_url and revoking the old link, and generates one if the app has none; delete is an idempotent soft-delete, and it is recoverable: the app stops serving at once but keeps every version, collection, row, attachment, member and grant, and restore brings all of it back until the retention window elapses (list_deleted shows what is in the trash and each app's purges_at deadline, which is null when the account is never purged). restore is idempotent on a live app, and refuses with 409 when the account is at its app limit, when the app was an expired trial, or when the owning account is itself deleted; an app an operator suspended comes back suspended, not active. purge destroys a deleted app and all its data immediately and forever, with no restore afterwards, and requires the app to already be deleted, so no single call takes a serving app to unrecoverable; wake wakes a dormant app and is otherwise a no-op that reports the actual status; domain_set binds a custom domain and returns the DNS records the domain owner must publish, where the first domain bound serves the app and every later one redirects to it, which is how apex plus www is configured; domain_status returns the serving domain and its `aliases`, live-refreshed against Cloudflare when that is enabled, with last_error carrying the reason a domain is not activating; domain_remove unbinds one domain, or all of them when no `domain` is given, and is idempotent.
Annotations the server declares
- title: Manage Apps
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
attachmentsBinary attachments (images, PDFs, audio, video) referenced from event payloads and input_data via `format: homespun-attachment-id`.1 finding
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The description is 2,115 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...capability URL, shown once, that a browser can GET without the caller's API key."
Description, as the server wrote it Binary attachments (images, PDFs, audio, video) referenced from event payloads and input_data via `format: homespun-attachment-id`. Actions: upload, fetch, presign, finalize, download, show, list, delete, mint_token, revoke_token, list_tokens. Choosing an upload path matters for cost. An inline upload with `content_base64` carries the bytes in the tool-call arguments, so they enter the model context at a token cost proportional to file size, paid again on every retry; a few-hundred-KB image is already expensive. Two paths avoid that entirely: fetch, when the bytes are reachable at a URL, and presign plus finalize, when the client can PUT the raw bytes out of band. Inline upload suits small assets and clients that have neither a URL nor an out-of-band PUT. fetch takes { source_url (https), scope } and the relay downloads the URL itself behind an SSRF guard (https only, no private, loopback or metadata hosts, DNS pinned, redirects refused, size-capped and timed out), then runs the same byte-sniff, allowlist, size, quota and scan checks as any upload. It works on any storage backend. upload takes either `content_base64` (base64 bytes, no filesystem) or `file_path` (an absolute path read on the relay host, so it only applies when the file is local to the relay). presign plus finalize is token-free: presign with { mime, size, sha256, scope } returns { put_url, attachment_id }, the caller PUTs the raw bytes to put_url over plain HTTP out of band, then finalize with the attachment_id. At finalize the relay re-reads the stored bytes, sniffs the real type, and enforces the same allowlist, size, sha256, quota and scan checks, so a presign that misstates its mime is caught and never served inline. The presigned path requires the Azure storage backend; a filesystem-backed relay returns a clear not-supported error and fetch or inline upload apply there instead. download writes to an absolute out_path or returns base64. An upload is scoped to agent (the default, reusable) or app. mint_token returns a /b/<token> capability URL, shown once, that a browser can GET without the caller's API key.
Annotations the server declares
- title: Manage Attachments
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
communityPublishing an app as a community template, taking your own listing back down, installing a template, and, for relay operators, reviewing submissions.1 finding
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The description is 2,313 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...ved version; reject takes a required note that lands in the publisher's app feed."
Description, as the server wrote it Publishing an app as a community template, taking your own listing back down, installing a template, and, for relay operators, reviewing submissions. Actions: publish, unpublish, get_config_contract, install, list_pending, get_submission, approve, reject, set_trust_level. publish captures a live app (html, manifest, the seed rows of its seedOnInstall collections, and listing metadata) into a pending template. It is installable by the returned direct link but is not listed in the public gallery until an operator approves it, and it requires a verified email and no more than a few pending submissions at once. Privacy consequence: an approved template's content and its captured seed rows become public to every platform user, so seed data in a published app must be example-only rather than real personal data. attest_example_only:true records that this was checked. A template may take a per-publisher `slug` (namespaced as <handle>/<slug>) and a semver `version` defaulting to 1.0.0, and a republish under the same slug must bump the version. unpublish is the publisher's own undo for a live listing, taken down by snapshot_id: it leaves the public gallery, search, and the direct snapshot install link. It works only on your own submissions, and a snapshot that does not exist or belongs to someone else reads as not found either way. Existing installs are unaffected, because an install is a fresh private copy rather than a live reference, so unpublishing never breaks an app someone already installed. It is idempotent, and publishing a new version is the way to put the listing back. get_config_contract reads what a template needs at install, meaning its settings collection and its ordered config and upload steps, by `ref`. install creates a fresh private copy of a template for the caller's owning human, passing answers as `config`, where a 'config' value is a string and an 'upload' value is a pre-uploaded attachment id from the attachments tool. The review actions are limited to the relay's configured community reviewers: list_pending returns the queue; get_submission returns a submission's full content by snapshot_id; approve lists it in the gallery, where a re-publish supersedes the app's prior approved version; reject takes a required note that lands in the publisher's app feed.
Annotations the server declares
- title: Community Templates
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
connectionsA v2 app's Connections: the stored credential (a static header token, or a full generic OAuth2 client) a manifest webhook rule authenticates its delivery target with, bound to a ho…4 findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The description mentions "credential".
What this check looks forMatched text
description: "A v2 app's Connections: the stored credential (a static header token, or a full gener..."
- Info
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "...e: store a webhook connection, a stored credential (static header token or a full generic..."
- Info
The input schema at properties.allowed_host.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.allowed_host.description: "...wildcard ("*.zohoapis.com"). The stored credential is attached to a delivery only when its..."
- Info
The input schema at properties.header_name.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.header_name.description: "create only (static). The header the credential rides in. Defaults to "Authorization"."
Description, as the server wrote it A v2 app's Connections: the stored credential (a static header token, or a full generic OAuth2 client) a manifest webhook rule authenticates its delivery target with, bound to a host so the credential can never be exfiltrated to another one. There is no update action: change a connection by deleting and recreating it. Actions: create stores a static or oauth2 connection and returns its metadata, never the secret; list returns the app's connections as metadata plus a non-reversible fingerprint, never any secret; delete is idempotent; consent_url builds (never fetches) the browser URL that completes an oauth2 connection's consent, since that is inherently a human-in-a-browser step an agent key cannot complete. A newly created oauth2 connection starts in `pending_auth` until the owner opens the consent_url and approves.
Annotations the server declares
- title: Manage App Connections
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
credentialsA v2 app's scoped service credentials (#1354, #1355): the bearer token an app owner points a backend they host themselves at, so their own server can read and write the app's data …10 findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The title mentions "Credentials".
What this check looks forMatched text
title: "Manage App Service Credentials"
- Info
The annotations title mentions "Credentials".
What this check looks forMatched text
annotations.title: "Manage App Service Credentials"
- Info
The description mentions "credentials".
What this check looks forMatched text
description: "A v2 app's scoped service credentials (#1354, #1355): the bearer token an app..."
- Info
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "mint: create a scoped service credential, the bearer token an app owner points a..."
- Info
The input schema at properties.mode.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.mode.description: "...an unnamed collection is denied, so the credential can never reach anything it was not han..."
- Info
The input schema at properties.grants.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.grants.description: "...which of read/create/update/delete this credential may attempt there (an entry may name ze..."
- Info
The input schema at properties.grants.items.properties.scope.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.grants.items.properties.scope.description: "...ows every row-addressed op to rows this credential itself wrote last. Inert for create."
- Info
The input schema at properties.members.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.members.description: "...pp's member directory appearing in this credential's boot/hello payloads. Defaults to fals..."
- Info
The input schema at properties.label.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.label.description: "...ptional owner-facing label shown in the credential list."
- Info
The input schema at properties.credential_id.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.credential_id.description: "pause / resume / rotate / revoke. The credential id (see list's `id` field)."
Description, as the server wrote it A v2 app's scoped service credentials (#1354, #1355): the bearer token an app owner points a backend they host themselves at, so their own server can read and write the app's data without holding the owner's full authority. Effective permission is always the intersection of the allowlist and what the app's owner could do, so a credential can only ever narrow, never widen, and it carries no role. Actions: mint creates one and returns its raw `token` shown once, never recoverable afterward (only its hash is stored); list returns the app's credentials with their allowlist and status, never any token material; pause reversibly stops one; resume undoes a pause (never a revoke, which is permanent); rotate issues a fresh token while the old one keeps working for an overlap window, so a running backend picks up the new token with no outage; revoke kills one permanently. Every action here is owner-or-owning-agent only: a service credential itself can reach none of these, by construction, so it can never mint or widen a sibling of itself.
Annotations the server declares
- title: Manage App Service Credentials
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
delete_rowSoft-delete a row from a v2 app's collection.No findings
Why it is in this group
- The name "delete_row" says "delete": a word for removing or ending something.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it Soft-delete a row from a v2 app's collection. 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 }.
Annotations the server declares
- title: Delete Row
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: true
- openWorldHint: false
grantsA v2 app's grant links (M5).No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it A v2 app's grant links (M5). 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.
Annotations the server declares
- title: Manage App Grant Links
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
ingestA v2 app's inbound catch-hooks (inbound-webhooks).No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it A v2 app's inbound catch-hooks (inbound-webhooks). 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.
Annotations the server declares
- title: Manage App Inbound Hooks
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
keyThe calling agent's API key.2 findings
Why it is in this group
- The description says "cannot be undone".
- The server marks it destructive (destructiveHint: true).
Findings
- Info
The description mentions "credential".
What this check looks forMatched text
description: "...hands a CLI or child process a working credential, and the raw value is not retrievable a..."
- Info
The input schema at properties.action.description mentions "credential".
What this check looks forMatched text
inputSchema.properties.action.description: "...hands a CLI or child process a working credential; the sibling is a distinct key that sho..."
Description, as the server wrote it The calling agent's API key. Actions: list returns key info (agent_id, key_prefix, timestamps); mint creates a sibling API key for the caller's own agent identity with the same scope and ownership and returns its raw value once, which is how an MCP-driven agent hands a CLI or child process a working credential, and the raw value is not retrievable afterwards, the sibling appears in a later list made with it, and the owner can revoke it; revoke destroys the agent's own key, which stops working immediately and cannot be undone, so it requires confirm:true. The relay derives identity from the caller's token, so every action applies to the caller's own agent and mint cannot target another agent's id.
Annotations the server declares
- title: Manage API Key
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
membersA 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.No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it 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. 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.
Annotations the server declares
- title: Manage App Members
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: true
- openWorldHint: true
publisherThe caller's community publisher identity: the @-handle and public profile shown in the template gallery.No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it The caller's community publisher identity: the @-handle and public profile shown in the template gallery. Actions: get returns the profile, including the handle, whether it has been claimed, tenure, and the rating and template counters; claim sets the handle from a lowercase 3-to-32-character string and may be used only once, after which the handle is permanent, and it refuses a handle that is reserved or already taken; update changes display_name, bio or url at any time. claim and update require a verified email. An existing publisher may hold a provisional `maker-...` handle assigned automatically, which claim renames on its one allowed use.
Annotations the server declares
- title: Publisher Profile
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
reviewRatings and reviews of community templates, responses from a template's own publisher, and, for relay operators, moderation.No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it Ratings and reviews of community templates, responses from a template's own publisher, and, for relay operators, moderation. Actions: create leaves a 1-to-5 star rating and an optional written body on a template the caller has installed, identifying it by `template` (\"<handle>/<slug>\") or by `handle` plus `slug`, and requires a verified email; each install yields exactly one review, and the aggregate carries across template versions. A body containing a link or a contact email is held automatically for a moderator before it appears. respond replies to a review of the caller's own template line (review_id plus response, or null to clear it), with one editable response per review. report flags a review for the relay's moderators (review_id plus reason) and is deduped per account. remove and unhold are limited to the relay's configured community reviewers: remove takes a review down and adjusts the rating aggregate, and unhold publishes a previously held review into the aggregate.
Annotations the server declares
- title: Community Reviews
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
tasteThe agent's UI taste notes: a short freeform markdown document of presentation preferences gathered from human feedback, such as 'denser layout' or 'no rounded corners'.No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it The agent's UI taste notes: a short freeform markdown document of presentation preferences gathered from human feedback, such as 'denser layout' or 'no rounded corners'. Reading it before generating or revising an app is what carries earlier feedback into new output. Actions: get returns the current document; set replaces it in whole, so it does not append; clear discards it. Scoped to presentation preferences rather than general storage.
Annotations the server declares
- title: Manage UI Taste Notes
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: false
transferA v2 app's ownership transfer (issue #1847): handing the app, and the quota and billing responsibility that come with it, to a different human.No findings
Why it is in this group
- The name "transfer" says "transfer": a word for a change that another call does not undo.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it A v2 app's ownership transfer (issue #1847): handing the app, and the quota and billing responsibility that come with it, to a different human. This is two steps, and `start` completing is only the first one. start mints a pending offer and emails the named person an accept link; the app is still owned here when the call returns, stays owned here while the offer is pending, and only moves once that person opens the link and accepts it. Reporting the transfer as done after `start` would be wrong: check `status` to see whether it is still pending or has dropped to null (accepted, expired, or withdrawn), and the caller's own agent key stops being able to deploy this app only at the moment it actually moves. Actions: start offers the app to an email address, 409ing if a transfer is already pending or the email already owns the app; status returns the pending transfer, or null if none; cancel withdraws a pending transfer and is idempotent, so cancelling with nothing pending is still a success.
Annotations the server declares
- title: Manage App Ownership Transfer
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true
update_rowUpdate an existing row in a v2 app's collection, replacing its data.No findings
Why it is in this group
- The name "update_row" says "update": a word for a change.
- The server marks it destructive (destructiveHint: true).
Description, as the server wrote it Update an existing row in a v2 app's collection, 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 }.
Annotations the server declares
- title: Update Row
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: true
- openWorldHint: false
Can't be undone 1Claude Code: ask
list_deleted_rowsList a collection's recently deleted rows: the recovery bin.1 finding
Why it is in this group
- The name "list_deleted_rows" says "list": a word for reading.
- The description says "cannot be restored".
- The server marks it read-only (readOnlyHint: true), but an annotation never lowers a class.
Findings
- Warn
The server marks "list_deleted_rows" read-only (readOnlyHint: true), and its own name, description or input give the class "irreversible".
What this check looks forMatched text
annotations: {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}; The name "list_deleted_rows" says "list": a word for reading.
Description, as the server wrote it List a collection's recently deleted rows: the recovery bin. 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 }.
Annotations the server declares
- title: List Deleted Rows
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
Changes things 5Claude Code: ask
add_rowCreate a row in a v2 app's collection, or return the existing row when `key` is already present (deduped:true).No findings
Why it is in this group
- The name "add_row" says "add": a word for a change.
Description, as the server wrote it Create a row in a v2 app's collection, 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. Passing `key` is also what makes a retry safe: a call unsure whether it already landed can repeat it and get the same row back rather than a duplicate. Without `key`, a retry mints a second row with its own server-generated key, since there is nothing to dedup against. The collection must be declared in the app's manifest with a `write` list that reaches the caller, which is the list that gates creates. An agent acts with its owner's authority, so `owner` covers it; there is no separate 'agent' role, and a manifest that names one is rejected. 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? }.
Annotations the server declares
- title: Add Row
- readOnlyHint: false
- destructiveHint: false
- idempotentHint: false
- openWorldHint: false
deploy_appDeploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL.1 finding
Why it is in this group
- The name "deploy_app" says "deploy": a word for a change.
Findings
- Info
The description is 4,505 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...bility, created } on create, or { app_id, version, compat, breaks? } on redeploy."
Description, as the server wrote it Deploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL. For sandbox-authored files in hosted chat, native file editing and shell tools can create and validate the document. Its UTF-8 byte length and SHA-256 go to `document_upload`; the exact raw bytes are PUT to the returned `upload_url` with `Authorization: Bearer <upload_token>`, and the returned `document_id` is passed here. This works from any network-capable shell and does not require installing the Homespun CLI. Use `dry_run:true` first, then deploy with the same document_id and parameters. The 15-minute ticket accepts only the declared bytes; identical ticket + deploy parameters replay the original result for 24 hours, while changed bytes or deploy parameters require a fresh upload. If the shell cannot reach the upload host, send inline `html`. A redeploy only needs the content that changed. Every content field is optional when `app_id` is given, and an omitted one keeps what is live: omit `manifest` for an HTML-only change, omit `html` or `document_id` for a manifest-only change, omit `assets` to keep the current files. This is the cheap path and the default, because an omitted field costs no output tokens at all: a one-line colour change does not resend the whole document, and a manifest edit does not resend it either. A field only needs sending when its content differs from what is live. `assets: []` is the explicit way to clear the asset set, and omitting every content field is refused, since there would be nothing to change. The extension keys used most often: app metadata; collections, with per-collection write, update, read and delete role lists, where write gates creates and also gates updates unless an update list is declared; externalHosts, a fetch allowlist; cdn, to allow CDN scripts and styles; capabilities, for Permissions-Policy opt-ins; embeds, an iframe frame-src allowlist; notify, for email-on-row rules; webhooks, for signed HTTP POST on-row rules; and agentTasks, to queue work for an agent running on the owner's own machine, described as a prompt rather than as code. The manifest grammar is documented in the Homespun guide that get_skill returns. Pass no `app_id` to create, which mints a slug and URL and requires a document source (`html`, `html_path`, or `document_id`) and `manifest`, or pass `app_id` to redeploy an existing app. `document_id` is mutually exclusive with `html` and `html_path`. Supply the HTML inline as `html`, through `document_id`, or as `html_path`, an absolute path read on the MCP-server host, which is the relay for a hosted connector or the CLI host for a locally-run one, and not the remote agent's machine; it avoids retransmitting a large HTML file on every deploy, only a locally-run connector can read it, and inline `html` wins if both are given. `dry_run:true` (alias `check`) validates only: it runs the full manifest and asset validation, the redeploy compat gate and the schedule-timezone advisory, then returns { ok, warnings, compat?, breaks? } without creating a version or mutating anything, and it resolves omitted fields the same way a real deploy would, so it reports on exactly the deploy that would run. A redeploy is refused with manifest_incompatible_redeploy, unless force:true, when it would strand rows already written (dropping a collection, tightening a schema, flipping appendOnly), or when it would widen what the app's install screen discloses: a collection's read reaching further than the live manifest, a capability added, cdn turned on, or a host added to externalHosts, embeds or a webhook target. The break quotes the sentence a user would now be asked to approve. Taking access away never prompts: dropping a role, dropping a capability, host or webhook, turning cdn off, or adding update:[\"creator\"] to a write:[\"anyone\"] collection, all redeploy clean. A removed collection is detached rather than deleted. Images, fonts, audio, video and data files ship with the app in the same call via `assets[]`. Each is validated and stored app-scoped and served at its `path` on the app's own origin, so the HTML references it by a stable same-origin path such as `<img src=\"frames/000.jpg\">`; media and font paths support HTTP Range for seeking. A redeploy's assets replace the previous version's set when sent, carry over when omitted, and are cleared by `assets: []`. Returns { app_id, slug, url, version, visibility, created } on create, or { app_id, version, compat, breaks? } on redeploy.
Annotations the server declares
- title: Deploy App
- readOnlyHint: false
- destructiveHint: false
- idempotentHint: false
- openWorldHint: true
document_uploadCreate a short-lived, byte-scoped upload session for an HTML document authored in a native editor or shell.1 finding
Why it is in this group
- The name "document_upload" says "upload": a word for a change.
Findings
- Info
The description mentions "credential".
What this check looks forMatched text
description: "...he declared bytes and is not an account credential. The returned `document_id` is used by..."
Description, as the server wrote it Create a short-lived, byte-scoped upload session for an HTML document authored in a native editor or shell. The caller supplies the exact UTF-8 byte length and SHA-256 hex digest as { size, sha256, app_id? }. The returned `upload_url` accepts a PUT of those raw UTF-8 bytes with `Authorization: Bearer <upload_token>`; this ticket can upload only the declared bytes and is not an account credential. The returned `document_id` is used by `deploy_app` and is mutually exclusive with `html` and `html_path`. The ticket expires after 15 minutes. A retry with the same ticket and deploy parameters returns the original deployed result for 24 hours; changed content or deploy parameters need a fresh upload session. If the shell cannot reach the upload URL, inline deployment remains available. The PUT works from any network-capable shell and does not require the Homespun CLI.
Annotations the server declares
- title: Upload App Document
- readOnlyHint: false
- destructiveHint: false
- idempotentHint: false
- openWorldHint: true
feedbackReports a problem with homespun itself to the relay operator, and lists what this agent has already reported.1 finding
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
Findings
- Info
The description is 2,126 characters long, over the 2,000 this check looks for.
What this check looks forMatched text
description ends: "...; list returns this agent's own submissions, newest first, paginated by `before`."
Description, as the server wrote it Reports a problem with homespun itself to the relay operator, and lists what this agent has already reported. A report is the operator's only visibility into a failure that happened inside an agent's session, so an unreported one is a failure nobody can fix. The channel covers homespun's own behaviour: a 5xx, or an error code the guide does not describe; a disagreement between documented and observed behaviour; something the tool surface cannot express, such as a missing capability or a schema that contradicts itself; an app misbehaving in a way that traces back to the platform (the bridge, the runtime, serving, the data API) rather than to authored HTML; or a guide that was wrong, ambiguous or silent. Outside its scope: the human's own task; bugs in an app the agent authored; presentation preferences, which belong in `taste`; the human's own configuration, such as a missing API key or the wrong account; and a 4xx caused by the agent's own arguments, except where the error message itself was misleading, which is a documentation problem best filed as a `note`. Duplicates cost the operator triage rather than adding signal. Action `list` returns this agent's own submissions, newest first, so a failure already recorded needs no second row: one report covers one distinct failure, however many times it was retried. The operator sees the row and not the session, so a bare "deploy failed" is not actionable. An actionable `message` carries the surface (mcp, cli, relay or app-runtime); where it happened (the tool or route); the skill version, from the `<!-- homespun skill vX.Y.Z -->` comment at the top of the guide; what was expected, in one line; what was observed, in one line carrying the exact error code and message; and the minimal steps or arguments that reproduce it. `type` is bug for something broken, feature for something missing, note for a rough edge or a confusing doc. `app_id` scopes a report to one app. There is no reply channel, so a report is not a route to an answer. Actions: create files one report; list returns this agent's own submissions, newest first, paginated by `before`.
Annotations the server declares
- title: Manage Feedback
- readOnlyHint: false
- destructiveHint: false
- idempotentHint: false
- openWorldHint: false
restore_rowRestore a soft-deleted row, undoing delete_row.No findings
Why it is in this group
- Nothing in its name or description says what it does, so it is counted as a write.
Description, as the server wrote it Restore a soft-deleted row, undoing delete_row. 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 }.
Annotations the server declares
- title: Restore Row
- readOnlyHint: false
- destructiveHint: false
- idempotentHint: false
- openWorldHint: false
Only reads 5Claude Code: allow
count_rowsThe live row count of a v2 app's collection (spec B4, issue #1056), a whole-scope total with no filter and no paging.No findings
Why it is in this group
- The name "count_rows" says "count": a word for reading.
Description, as the server wrote it The live row count of a v2 app's collection (spec B4, issue #1056), a whole-scope total with no filter and no paging. Gated by the collection's countRead opt-in, independent of its read list: a collection that opted in returns its count even to a caller who cannot list the rows (the '3 spots left' shape), and a collection that never opted in refuses with collection_count_forbidden even for a caller who could otherwise list. Returns { count }.
Annotations the server declares
- title: Count Rows
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
get_feed_eventsPoll a v2 app's change feed for what has happened: row creates, updates and deletes, from any writer, agent or human.No findings
Why it is in this group
- The name "get_feed_events" says "get": a word for reading.
Description, as the server wrote it Poll a v2 app's change feed for 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 }.
Annotations the server declares
- title: Get App Feed Events
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
get_rowFetch a single row by its key from a v2 app collection, through a dedicated relay route rather than a client-side scan.No findings
Why it is in this group
- The name "get_row" says "get": a word for reading.
Description, as the server wrote it Fetch a single row by its key from a v2 app collection, through a dedicated relay route rather than a client-side scan. Returns { row }, or an isError row_not_found.
Annotations the server declares
- title: Get Row
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
get_skillThe relay's SKILL.md, a generated guide to the Homespun workflow covering events versus records, the schema grammars and the poll loop.No findings
Why it is in this group
- The name "get_skill" says "get": a word for reading.
Description, as the server wrote it The relay's SKILL.md, a generated guide to the Homespun workflow covering events versus records, the schema grammars and the poll loop. Needs no API key. Useful when working out how the other tools fit together, or to refresh a cached copy. Pass version_only:true to return just the relay's skill version string, which is enough to tell whether a cached copy is current.
Annotations the server declares
- title: Get Skill Guide
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
list_rowsList rows in a v2 app's mutable collection.No findings
Why it is in this group
- The name "list_rows" says "list": a word for reading.
Description, as the server wrote it List rows in a v2 app's mutable collection. 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 }.
Annotations the server declares
- title: List Rows
- readOnlyHint: true
- destructiveHint: false
- idempotentHint: true
- openWorldHint: false
This is not a safety rating, and no finding is not a safety guarantee. The groups and findings come from fixed checks of the tool definitions the server returned on the date shown, not from its code or what it does when called. How the checks work.
Install
Pinned to version 1.6.98. Values in angle brackets are yours to fill in.
claude mcp add --transport http homespun https://homespun.dev/mcpRun in a terminal.
Claude Code permissions
Allow the reads, ask before a change, deny the deletes. Paste into .claude/settings.json.
{
"permissions": {
"allow": [
"mcp__homespun__list_rows",
"mcp__homespun__count_rows",
"mcp__homespun__get_row",
"mcp__homespun__get_feed_events",
"mcp__homespun__get_skill"
],
"ask": [
"mcp__homespun__document_upload",
"mcp__homespun__deploy_app",
"mcp__homespun__add_row",
"mcp__homespun__list_deleted_rows",
"mcp__homespun__restore_row",
"mcp__homespun__feedback"
],
"deny": [
"mcp__homespun__update_row",
"mcp__homespun__delete_row",
"mcp__homespun__apps",
"mcp__homespun__members",
"mcp__homespun__grants",
"mcp__homespun__transfer",
"mcp__homespun__credentials",
"mcp__homespun__connections",
"mcp__homespun__ingest",
"mcp__homespun__attachments",
"mcp__homespun__taste",
"mcp__homespun__key",
"mcp__homespun__agent",
"mcp__homespun__community",
"mcp__homespun__publisher",
"mcp__homespun__review"
]
}
}Tools that only read are allowed, tools that change things ask first, tools that delete or cancel are denied. It assumes the server is installed as homespun. Review it before you use it.