@homespunapps/mcp 1.6.47 → 1.6.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HomespunClient } from "@homespunapps/core";
2
2
  /**
3
- * The hosted Homespun relay — the URL fallback when nothing else is set. A
4
- * self-hoster overrides it with HOMESPUN_URL or a registered profile.
3
+ * The hosted Homespun relay — the URL fallback when nothing else is set. An
4
+ * operator overrides it with HOMESPUN_URL or a registered profile.
5
5
  */
6
6
  export declare const DEFAULT_RELAY_URL = "https://homespun.dev";
7
7
  /**
@@ -51,7 +51,7 @@ export declare function describeActiveConfig(): {
51
51
  * CLI and any later MCP launch reuse the same identity, and the human never
52
52
  * has to run `homespun agent register` by hand.
53
53
  *
54
- * A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
54
+ * An operator on a `secret`-mode relay (or anyone who prefers explicit
55
55
  * provisioning) sets HOMESPUN_API_KEY / HOMESPUN_TOKEN and the auto-register path is
56
56
  * never taken.
57
57
  *
package/dist/config.js CHANGED
@@ -22,8 +22,8 @@ import { homedir } from "node:os";
22
22
  import { join, dirname } from "node:path";
23
23
  import { HomespunClient, registerAgent } from "@homespunapps/core";
24
24
  /**
25
- * The hosted Homespun relay — the URL fallback when nothing else is set. A
26
- * self-hoster overrides it with HOMESPUN_URL or a registered profile.
25
+ * The hosted Homespun relay — the URL fallback when nothing else is set. An
26
+ * operator overrides it with HOMESPUN_URL or a registered profile.
27
27
  */
28
28
  export const DEFAULT_RELAY_URL = "https://homespun.dev";
29
29
  /**
@@ -202,7 +202,7 @@ function resolveApiKey() {
202
202
  * CLI and any later MCP launch reuse the same identity, and the human never
203
203
  * has to run `homespun agent register` by hand.
204
204
  *
205
- * A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
205
+ * An operator on a `secret`-mode relay (or anyone who prefers explicit
206
206
  * provisioning) sets HOMESPUN_API_KEY / HOMESPUN_TOKEN and the auto-register path is
207
207
  * never taken.
208
208
  *
package/dist/tools.js CHANGED
@@ -795,13 +795,16 @@ const communityShape = {
795
795
  "unpublish",
796
796
  "get_config_contract",
797
797
  "install",
798
+ "upgrade_check",
799
+ "upgrade",
800
+ "revert",
798
801
  "list_pending",
799
802
  "get_submission",
800
803
  "approve",
801
804
  "reject",
802
805
  "set_trust_level",
803
806
  ])
804
- .describe("publish: publishes one of the caller's apps as a community template (app_id; optional title/description/category/tags). Privacy consequence: publishing makes the template content and the captured seed rows (the live rows of every seedOnInstall collection, captured at publish time) public to every platform user once approved, so an app whose seedOnInstall collections hold real personal data (names, emails, addresses, messages, anything private) is not safe to publish: seed data must be example-only. attest_example_only:true records that this was checked. The capture (html + manifest + seed rows) lands pending review, installable by its returned direct link but not listed until approved; an established publisher is fast-tracked, and the response's expedited/auto_approved fields report which path it took. unpublish: takes one of the caller's own published templates back down (snapshot_id). It removes the listing from the public gallery, from search, and from the direct snapshot install link. Existing installs keep working untouched, because an install is a fresh private copy rather than a live reference. It is idempotent (unpublishing an already-unpublished template is a no-op), and a snapshot that does not exist or belongs to someone else reads as not found either way. Publishing a new version is what puts the listing back. get_config_contract: read a template's install-time config contract by `ref` (a namespaced '<handle>/<slug>' or a snapshot id): its settings_collection, ordered config_steps (each with key/kind/required/secret/choices/default), and connect_steps (inbound hooks the app receives on). An 'upload' step wants a file, pre-uploaded with the attachments tool (scope agent) and passed as its attachment id. A template installed with connect_steps provisions hook URLs, which the `ingest` tool's list action returns for the new app_id, ready to wire into the external service. install: installs a template by `ref` for the caller, whose owning human becomes the owner. `config` is { stepKey: value } from the contract: a 'config' step's value is a string, an 'upload' step's value is a pre-uploaded attachment id. An omitted required step is rejected. Returns the new app's id, slug, and url; installs always create a fresh private copy. list_pending / get_submission / approve / reject / set_trust_level are relay-operator-only review actions: list_pending (the review queue, expedited submissions first), get_submission (a submission's full html+manifest+seedRows plus external_destinations, the hosts it can send data to or pull data from, by snapshot_id), approve (snapshot_id, lists it in the gallery + supersedes the app's prior approved version), reject (snapshot_id + a required note that lands in the publisher's app feed), set_trust_level (promote/demote a publisher by handle: handle + trust_level 'new'|'established')."),
807
+ .describe("publish: publishes one of the caller's apps as a community template (app_id; optional title/description/category/tags). Privacy consequence: publishing makes the template content and the captured seed rows (the live rows of every seedOnInstall collection, captured at publish time) public to every platform user once approved, so an app whose seedOnInstall collections hold real personal data (names, emails, addresses, messages, anything private) is not safe to publish: seed data must be example-only. attest_example_only:true records that this was checked. The capture (html + manifest + seed rows) lands pending review, installable by its returned direct link but not listed until approved; an established publisher is fast-tracked, and the response's expedited/auto_approved fields report which path it took. unpublish: takes one of the caller's own published templates back down (snapshot_id). It removes the listing from the public gallery, from search, and from the direct snapshot install link. Existing installs keep working untouched, because an install is a fresh private copy rather than a live reference. It is idempotent (unpublishing an already-unpublished template is a no-op), and a snapshot that does not exist or belongs to someone else reads as not found either way. Publishing a new version is what puts the listing back. get_config_contract: read a template's install-time config contract by `ref` (a namespaced '<handle>/<slug>' or a snapshot id): its settings_collection, ordered config_steps (each with key/kind/required/secret/choices/default), and connect_steps (inbound hooks the app receives on). An 'upload' step wants a file, pre-uploaded with the attachments tool (scope agent) and passed as its attachment id. A template installed with connect_steps provisions hook URLs, which the `ingest` tool's list action returns for the new app_id, ready to wire into the external service. install: installs a template by `ref` for the caller, whose owning human becomes the owner. `config` is { stepKey: value } from the contract: a 'config' step's value is a string, an 'upload' step's value is a pre-uploaded attachment id. An omitted required step is rejected. Returns the new app's id, slug, and url; installs always create a fresh private copy. list_pending / get_submission / approve / reject / set_trust_level are relay-operator-only review actions: list_pending (the review queue, expedited submissions first), get_submission (a submission's full html+manifest+seedRows plus external_destinations, the hosts it can send data to or pull data from, by snapshot_id), approve (snapshot_id, lists it in the gallery + supersedes the app's prior approved version), reject (snapshot_id + a required note that lands in the publisher's app feed), set_trust_level (promote/demote a publisher by handle: handle + trust_level 'new'|'established').\n\nupgrade_check / upgrade / revert keep an already-installed app current with its source template (app_id). An install is a one-shot fork, so nothing updates on its own and there is no follow/pin: you have to ask. upgrade_check reports whether a newer live version of that app's template line exists, whether it would apply cleanly, and what it would newly be allowed to reach. upgrade applies it in place, keeping the app's address, collections and rows, and landing as a new version you can undo. It refuses outright, with no override, when the new version would strand rows the app already holds; when the new version merely asks for more than the installed one, pass accept_permissions:true, but only after showing the owner what `permission_lines` says. revert puts the app back on the version it ran before the last update, and refuses when rows written since would have nowhere to live under the older one."),
805
808
  ref: z
806
809
  .string()
807
810
  .optional()
@@ -813,7 +816,15 @@ const communityShape = {
813
816
  app_id: z
814
817
  .string()
815
818
  .optional()
816
- .describe("publish only. The id of an app the caller owns, to publish."),
819
+ .describe("publish / upgrade_check / upgrade / revert. For publish, the app to publish. For the three upgrade actions, the installed app to act on: an installed template is a fork, so the question is whether a newer version of the template that app came from exists, which only the app can answer."),
820
+ accept_permissions: z
821
+ .boolean()
822
+ .optional()
823
+ .describe("upgrade only. Required when upgrade_check reports a non-empty `permissions` diff, meaning the new version asks for more than the installed one (new hosts it can send data to, new device capabilities, a service worker, CDN scripts). Never assume it: show the owner `permission_lines` and set this only once they have agreed. It does not clear a version that would strand rows, which nothing can."),
824
+ expect_version: z
825
+ .string()
826
+ .optional()
827
+ .describe("upgrade only. The version upgrade_check reported. When given, the upgrade is refused if the offer has moved since, so a publisher shipping again mid-flight cannot slip a version past you that you never showed anyone."),
817
828
  title: z
818
829
  .string()
819
830
  .optional()
@@ -1144,14 +1155,20 @@ export const TOOLS = [
1144
1155
  },
1145
1156
  {
1146
1157
  name: "upsert_row",
1147
- description: "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. 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? }.",
1158
+ description: "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 '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? }.",
1148
1159
  inputSchema: upsertRowShape,
1149
1160
  annotations: {
1150
1161
  title: "Upsert Row",
1151
1162
  readOnlyHint: false,
1152
1163
  // Create-or-return-existing. Removes nothing.
1153
1164
  destructiveHint: false,
1154
- idempotentHint: true,
1165
+ // NOT idempotent as a tool-level default: idempotence here is a
1166
+ // per-call property of `key`, not a per-tool one, and a single boolean
1167
+ // cannot express "safe when this argument is passed, unsafe
1168
+ // otherwise." A call made without `key` mints a second row on retry,
1169
+ // so the safe default is false; the description spells out the
1170
+ // `key` condition that makes a retry safe, for a caller that reads it.
1171
+ idempotentHint: false,
1155
1172
  openWorldHint: false,
1156
1173
  },
1157
1174
  handler: async (client, args) => {
@@ -1304,7 +1321,12 @@ export const TOOLS = [
1304
1321
  readOnlyHint: false,
1305
1322
  // Destructive: `delete` removes an app.
1306
1323
  destructiveHint: true,
1307
- idempotentHint: true,
1324
+ // NOT idempotent: `share_link_rotate` issues a new share token and
1325
+ // revokes the old link on every call, so a retried call invalidates a
1326
+ // link someone may already hold and mints a different one rather than
1327
+ // having no additional effect. Matches the `grants` tool, which is the
1328
+ // same shape.
1329
+ idempotentHint: false,
1308
1330
  openWorldHint: false,
1309
1331
  },
1310
1332
  handler: async (client, args) => {
@@ -1482,7 +1504,11 @@ export const TOOLS = [
1482
1504
  readOnlyHint: false,
1483
1505
  // Destructive: `revoke` kills a live capability URL.
1484
1506
  destructiveHint: true,
1485
- idempotentHint: true,
1507
+ // NOT idempotent: `mint` creates a fresh capability URL each call, so
1508
+ // a retried call leaves a second live grant link behind rather than
1509
+ // having no additional effect. Matches the `key` tool, which is the
1510
+ // same shape.
1511
+ idempotentHint: false,
1486
1512
  openWorldHint: false,
1487
1513
  },
1488
1514
  handler: async (client, args) => {
@@ -1837,7 +1863,7 @@ export const TOOLS = [
1837
1863
  // ----- consolidated management tools --------------------------------------
1838
1864
  {
1839
1865
  name: "attachments",
1840
- description: "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.\n\nChoosing 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.\n\nfetch 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 self-host 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.",
1866
+ description: "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.\n\nChoosing 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.\n\nfetch 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.",
1841
1867
  inputSchema: attachmentsShape,
1842
1868
  // Consolidated tool: read actions (download/show/list/list_tokens) +
1843
1869
  // mutating ones (upload/delete/mint_token/revoke_token). openWorld:true
@@ -2263,6 +2289,37 @@ export const TOOLS = [
2263
2289
  : undefined;
2264
2290
  return jsonResult(await client.installCommunityTemplate(ref, config));
2265
2291
  }
2292
+ case "upgrade_check": {
2293
+ const appId = str(args, "app_id");
2294
+ if (appId === undefined) {
2295
+ return invalidArgs("upgrade_check requires `app_id`");
2296
+ }
2297
+ return jsonResult(await client.checkTemplateUpgrade(appId));
2298
+ }
2299
+ case "upgrade": {
2300
+ const appId = str(args, "app_id");
2301
+ if (appId === undefined) {
2302
+ return invalidArgs("upgrade requires `app_id`");
2303
+ }
2304
+ const opts = {};
2305
+ // Only ever set when the caller passed it. An absent field must
2306
+ // not satisfy the consent gate, or an agent could accept a
2307
+ // widening on its owner's behalf by simply not mentioning it.
2308
+ if (args["accept_permissions"] === true) {
2309
+ opts.acceptPermissions = true;
2310
+ }
2311
+ const expect = str(args, "expect_version");
2312
+ if (expect !== undefined)
2313
+ opts.expectVersion = expect;
2314
+ return jsonResult(await client.upgradeTemplate(appId, opts));
2315
+ }
2316
+ case "revert": {
2317
+ const appId = str(args, "app_id");
2318
+ if (appId === undefined) {
2319
+ return invalidArgs("revert requires `app_id`");
2320
+ }
2321
+ return jsonResult(await client.revertTemplateUpgrade(appId));
2322
+ }
2266
2323
  case "list_pending": {
2267
2324
  const opts = {};
2268
2325
  if (args["limit"] !== undefined)
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.6.47";
1
+ export declare const VERSION = "1.6.49";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Single source of the package version, reported in the MCP server's
2
2
  // serverInfo. Kept in sync with package.json by the release tooling.
3
- export const VERSION = "1.6.47";
3
+ export const VERSION = "1.6.49";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@homespunapps/mcp",
3
3
  "mcpName": "dev.homespun/homespun",
4
- "version": "1.6.47",
4
+ "version": "1.6.49",
5
5
  "description": "Model Context Protocol (stdio) server for Homespun: lets any MCP client (Claude Desktop, Cursor, …) deploy a multi-user web app with hosting, auth, a shared database and permissions included.",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -40,13 +40,13 @@
40
40
  ],
41
41
  "scripts": {
42
42
  "build": "tsc",
43
- "typecheck": "tsc --noEmit",
43
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
44
44
  "test": "vitest run",
45
45
  "test:unit": "vitest run"
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.30.0",
49
- "@homespunapps/core": "^1.6.47",
49
+ "@homespunapps/core": "^1.6.49",
50
50
  "zod": "^4.4.3"
51
51
  },
52
52
  "devDependencies": {
package/server.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "name": "dev.homespun/homespun",
4
4
  "title": "Homespun",
5
5
  "description": "Deploy a multi-user web app from your agent: hosting, auth, database, and permissions.",
6
- "version": "1.6.47",
7
- "websiteUrl": "https://docs.homespun.dev",
6
+ "version": "1.6.49",
7
+ "websiteUrl": "https://homespun.dev",
8
8
  "packages": [
9
9
  {
10
10
  "registryType": "npm",
11
11
  "registryBaseUrl": "https://registry.npmjs.org",
12
12
  "identifier": "@homespunapps/mcp",
13
- "version": "1.6.47",
13
+ "version": "1.6.49",
14
14
  "transport": {
15
15
  "type": "stdio"
16
16
  },