@homespunapps/mcp 1.6.34 → 1.6.36

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/tools.js CHANGED
@@ -162,7 +162,7 @@ const deployAppShape = {
162
162
  .string()
163
163
  .min(1)
164
164
  .optional()
165
- .describe("The app's UI as a complete HTML document (single file, up to the relay's size cap), sent inline. Provide either this or `html_path`. Inline is required for a hosted or remote connector that has no filesystem. If both are given, inline `html` wins."),
165
+ .describe("The app's UI as a complete HTML document (single file, up to the relay's size cap), sent inline. Provide either this or `html_path`. Inline is required for a hosted or remote connector that has no filesystem. If both are given, inline `html` wins. ON REDEPLOY, omit it entirely to keep the live document: a manifest-only change (adding a collection, widening externalHosts) then costs nothing in HTML."),
166
166
  html_path: z
167
167
  .string()
168
168
  .optional()
@@ -172,7 +172,20 @@ const deployAppShape = {
172
172
  .optional()
173
173
  .describe("Validate only: run the full manifest + asset-shape validation, the compat gate (for a redeploy), and the schedule-timezone advisory, then return { ok, warnings, compat?, breaks? } WITHOUT creating a version or mutating anything. An invalid manifest returns the SAME error a real deploy would; a redeploy the compat gate would refuse reports the break instead of applying it. `check` is an accepted alias."),
174
174
  check: z.boolean().optional().describe("Alias for `dry_run`."),
175
- manifest: jsonObjectSchema.describe("The x-homespun-manifest capability document (a JSON object). Eight extension keys: app metadata; collections (+ per-collection write/update/read/delete role lists, where write gates creates and also updates unless the optional update list is declared); externalHosts (fetch allowlist); cdn (allow CDN scripts/styles); capabilities (Permissions-Policy opt-ins); embeds (iframe frame-src allowlist); notify (email-on-row rules); webhooks (signed HTTP POST on-row rules). Call get_skill for the full grammar before authoring one from scratch."),
175
+ // Optional at the SCHEMA level because a redeploy inherits an omitted
176
+ // manifest (#1272); the handler still refuses a create without one.
177
+ //
178
+ // `z.preprocess` rather than `.nullish()` on purpose. MCP clients routinely
179
+ // send `manifest: null` instead of dropping the key, and the SDK validates
180
+ // arguments against this shape before the handler ever runs, so null has to
181
+ // be acceptable here. But `.nullish()` emits `anyOf:[{type:"object"},
182
+ // {type:"null"}]`, which loses the plain top-level `type: "object"` that
183
+ // stops a harness from stringifying the manifest (the reported bug the
184
+ // advertised-schema test guards). Preprocessing keeps the emitted schema
185
+ // exactly `type: "object"` AND turns a null into an omission.
186
+ manifest: z
187
+ .preprocess((v) => (v === null ? undefined : v), jsonObjectSchema.optional())
188
+ .describe("The x-homespun-manifest capability document (a JSON object). REQUIRED to create; ON REDEPLOY, omit it to keep the live manifest, which is what most redeploys want (the manifest was byte-identical to the previous version in 71% of real redeploys). Eight extension keys: app metadata; collections (+ per-collection write/update/read/delete role lists, where write gates creates and also updates unless the optional update list is declared); externalHosts (fetch allowlist); cdn (allow CDN scripts/styles); capabilities (Permissions-Policy opt-ins); embeds (iframe frame-src allowlist); notify (email-on-row rules); webhooks (signed HTTP POST on-row rules). Call get_skill for the full grammar before authoring one from scratch."),
176
189
  visibility: z
177
190
  .enum(["private", "link", "public"])
178
191
  .optional()
@@ -209,7 +222,7 @@ const deployAppShape = {
209
222
  }),
210
223
  ]))
211
224
  .optional()
212
- .describe("Optional bundle of files shipped WITH the app in ONE deploy: images, fonts, audio/video, data. Each asset either carries its bytes inline as `content_base64` OR references an already-uploaded attachment by `attachment_id` (prefer the reference form for real images/media: upload once via `attachments fetch` or presign, then bind it here with NO base64 in the deploy body). Each asset is validated + stored app-scoped exactly like a normal attachment (byte-sniff, allowlist, size cap, quota, scan) and served at its `path` on the app's OWN origin, so the page references it by a stable same-origin path (`<img src=\"frames/000.jpg\">`, `<video src=\"media/intro.mp4\">`; media/font paths support HTTP Range). The whole deploy is rejected atomically if any asset fails validation. A redeploy's assets REPLACE the previous version's set. Bounded by the relay's per-deploy asset-count cap; total bytes by the per-app blob quota."),
225
+ .describe('Optional bundle of files shipped WITH the app in ONE deploy: images, fonts, audio/video, data. Each asset either carries its bytes inline as `content_base64` OR references an already-uploaded attachment by `attachment_id` (prefer the reference form for real images/media: upload once via `attachments fetch` or presign, then bind it here with NO base64 in the deploy body). Each asset is validated + stored app-scoped exactly like a normal attachment (byte-sniff, allowlist, size cap, quota, scan) and served at its `path` on the app\'s OWN origin, so the page references it by a stable same-origin path (`<img src="frames/000.jpg">`, `<video src="media/intro.mp4">`; media/font paths support HTTP Range). The whole deploy is rejected atomically if any asset fails validation. ON REDEPLOY, assets you send REPLACE the previous version\'s set, omitting `assets` keeps the live set (no re-upload, no re-encoding), and `assets: []` is the explicit way to clear it. Bounded by the relay\'s per-deploy asset-count cap; total bytes by the per-app blob quota.'),
213
226
  };
214
227
  const listRowsShape = {
215
228
  app_id: z.string().min(1).describe("The app id."),
@@ -764,7 +777,7 @@ export const TOOLS = [
764
777
  // ----- v2 app lifecycle + data (discrete, hot-path) -----------------------
765
778
  {
766
779
  name: "deploy_app",
767
- description: "Deploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL. The manifest carries eight extension keys: 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; and webhooks, for signed HTTP POST on-row rules. The manifest grammar is documented in the Homespun guide that get_skill returns.\n\nPass no `app_id` to create, which mints a slug and URL, or pass `app_id` to redeploy an existing app with new content. Supply the HTML inline as `html`, 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-shape validation, the redeploy compat gate and the schedule-timezone advisory, then returns { ok, warnings, compat?, breaks? } without creating a version or mutating anything.\n\nA 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, such as a collection's read reaching further than the live manifest. The break quotes the sentence a user would now be asked to approve. Taking access away never prompts: dropping a role, or adding update:[\\\"creator\\\"] to a write:[\\\"anyone\\\"] collection, redeploys clean. A removed collection is detached rather than deleted.\n\nImages, 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.\n\nReturns { app_id, slug, url, version, visibility, created } on create, or { app_id, version, compat, breaks? } on redeploy.",
780
+ description: "Deploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL.\n\nON REDEPLOY, SEND ONLY WHAT CHANGES. 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` for a manifest-only change, omit `assets` to keep the current files. This is the cheap path and it is the one to reach for by default, because the omitted field costs no output tokens at all: a one-line colour change should not resend the whole document, and a manifest edit should not resend it either. Send a field only when its content is different from what is live. `assets: []` is the explicit way to clear the asset set, and omitting all three is refused, since there would be nothing to change.\n\nThe manifest carries eight extension keys: 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; and webhooks, for signed HTTP POST on-row rules. The manifest grammar is documented in the Homespun guide that get_skill returns.\n\nPass no `app_id` to create, which mints a slug and URL and requires both `html` and `manifest`, or pass `app_id` to redeploy an existing app. Supply the HTML inline as `html`, 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.\n\nA 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, such as a collection's read reaching further than the live manifest. The break quotes the sentence a user would now be asked to approve. Taking access away never prompts: dropping a role, or adding update:[\\\"creator\\\"] to a write:[\\\"anyone\\\"] collection, redeploys clean. A removed collection is detached rather than deleted.\n\nImages, 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: []`.\n\nReturns { app_id, slug, url, version, visibility, created } on create, or { app_id, version, compat, breaks? } on redeploy.",
768
781
  inputSchema: deployAppShape,
769
782
  annotations: {
770
783
  title: "Deploy App",
@@ -803,14 +816,21 @@ export const TOOLS = [
803
816
  const dryRun = args["dry_run"] === true || args["check"] === true;
804
817
  const assets = args["assets"];
805
818
  const appId = str(args, "app_id");
819
+ // `manifest: null` is how several MCP clients express "not sending
820
+ // this" rather than dropping the key, so it means the same as omitted
821
+ // here (the relay's resolver applies the same rule).
822
+ const manifestValue = manifest.value === null ? undefined : manifest.value;
806
823
  if (appId === undefined) {
807
824
  if (html === undefined) {
808
825
  return invalidArgs("create requires `html` or `html_path`");
809
826
  }
827
+ if (manifestValue === undefined) {
828
+ return invalidArgs("create requires `manifest` (there is nothing to inherit on a first deploy; inherit-on-omit applies to a redeploy, which passes `app_id`)");
829
+ }
810
830
  if (dryRun) {
811
831
  return jsonResult(await client.checkDeploy({
812
832
  html,
813
- manifest: manifest.value,
833
+ manifest: manifestValue,
814
834
  assets,
815
835
  }));
816
836
  }
@@ -821,20 +841,24 @@ export const TOOLS = [
821
841
  }
822
842
  return jsonResult(await client.deployApp({
823
843
  html,
824
- manifest: manifest.value,
844
+ manifest: manifestValue,
825
845
  visibility,
826
846
  slug,
827
847
  assets,
828
848
  }));
829
849
  }
830
- if (html === undefined) {
831
- return invalidArgs("redeploy requires `html` or `html_path`");
850
+ // REDEPLOY. Nothing is required individually: an omitted html,
851
+ // manifest or assets keeps what is live (issue #1272). Only the empty
852
+ // body is refused, and locally, so the caller gets the reason rather
853
+ // than a round trip that says the same thing.
854
+ if (html === undefined && manifestValue === undefined && !assets) {
855
+ return invalidArgs("a redeploy must change something: send `html`, `manifest` or `assets` (an omitted field keeps what is live; `assets: []` clears the asset set)");
832
856
  }
833
857
  if (dryRun) {
834
858
  return jsonResult(await client.checkDeploy({
835
859
  app_id: appId,
836
860
  html,
837
- manifest: manifest.value,
861
+ manifest: manifestValue,
838
862
  force: args["force"],
839
863
  assets,
840
864
  }));
@@ -844,7 +868,7 @@ export const TOOLS = [
844
868
  }
845
869
  const redeployed = await client.redeployApp(appId, {
846
870
  html,
847
- manifest: manifest.value,
871
+ manifest: manifestValue,
848
872
  force: args["force"],
849
873
  assets,
850
874
  });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.6.34";
1
+ export declare const VERSION = "1.6.36";
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.34";
3
+ export const VERSION = "1.6.36";
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.34",
4
+ "version": "1.6.36",
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",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.20.0",
49
- "@homespunapps/core": "^1.6.34",
49
+ "@homespunapps/core": "^1.6.36",
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.34",
6
+ "version": "1.6.36",
7
7
  "websiteUrl": "https://docs.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.34",
13
+ "version": "1.6.36",
14
14
  "transport": {
15
15
  "type": "stdio"
16
16
  },