@homespunapps/mcp 1.6.38 → 1.6.39

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/README.md CHANGED
@@ -125,7 +125,7 @@ To keep the tool list compact (a flat 50+ tools would bloat client context and d
125
125
  | `key` | `list` · `mint` · `revoke` |
126
126
  | `feedback` | `create` · `list` |
127
127
  | `agent` | `whoami` · `claim` · `logout` |
128
- | `community` | `publish` · `get_config_contract` · `install` · `list_pending` · `get_submission` · `approve` · `reject` · `set_trust_level` |
128
+ | `community` | `publish` · `unpublish` · `get_config_contract` · `install` · `list_pending` · `get_submission` · `approve` · `reject` · `set_trust_level` |
129
129
  | `publisher` | `claim` · `get` · `update` |
130
130
  | `review` | `create` · `respond` · `report` · `remove` · `unhold` |
131
131
 
package/dist/tools.d.ts CHANGED
@@ -14,6 +14,48 @@ export interface ToolResult {
14
14
  isError?: boolean;
15
15
  [key: string]: unknown;
16
16
  }
17
+ /**
18
+ * Read the structured error code off a ToolResult produced by
19
+ * errorResult()/invalidArgs(). Undefined for a success, and for an `isError`
20
+ * result built by hand somewhere else.
21
+ */
22
+ export declare function toolErrorCode(result: ToolResult): string | undefined;
23
+ /** Outcome of one tool invocation, as reported to {@link ToolEnv.onToolResult}. */
24
+ export interface ToolCallReport {
25
+ /** Registered tool name (a bounded set — safe as a metric label). */
26
+ tool: string;
27
+ /**
28
+ * `ok` — handler returned without `isError`.
29
+ * `error` — handler returned a structured `isError` result.
30
+ * `exception` — handler THREW. Always a bug: every handler is meant to
31
+ * catch and return errorResult().
32
+ */
33
+ outcome: "ok" | "error" | "exception";
34
+ /**
35
+ * Structured code for a failure: the relay's ApiError code for a
36
+ * HomespunApiError, "invalid_args" for a rejected argument, "internal" for a
37
+ * bare throw. Undefined on success, and on an `isError` result that carries
38
+ * no tag.
39
+ */
40
+ errorCode?: string;
41
+ /** Wall-clock milliseconds the handler took. */
42
+ ms: number;
43
+ }
44
+ /**
45
+ * Invoke a tool handler and report its outcome to `env.onToolResult`.
46
+ *
47
+ * The seam that makes remote MCP calls observable (issue #1287): a tool
48
+ * failure is an `isError` result inside an HTTP 200, so a host that just
49
+ * awaits `tool.handler(...)` cannot tell a failed deploy_app from a successful
50
+ * one. This times the call, catches a throw, and reads the structured error
51
+ * code off the returned result.
52
+ *
53
+ * Transport-agnostic by construction: `onToolResult` is optional, so the stdio
54
+ * CLI server can keep calling `tool.handler` directly (or adopt this and opt
55
+ * in later) with no behaviour change. The handler's result — or its thrown
56
+ * error — is passed through untouched.
57
+ */
58
+ export declare function runTool(tool: ToolDef, client: HomespunClient, args: Record<string, unknown>, env?: ToolEnv): Promise<ToolResult>;
17
59
  /**
18
60
  * Host-supplied capabilities for the handful of tools that aren't pure
19
61
  * HomespunClient wrappers. The stdio server leaves this undefined and the
@@ -51,6 +93,17 @@ export interface ToolEnv {
51
93
  * e.g. deploy_app html_path=/app/.env.
52
94
  */
53
95
  hostFsReads?: boolean;
96
+ /**
97
+ * Optional per-call observability hook, fired by {@link runTool} once the
98
+ * handler settles (issue #1287). The hosted relay uses it to log the tool
99
+ * name, outcome and structured error code, and to tick its
100
+ * homespun_mcp_tool_calls_total counter — none of which is recoverable from
101
+ * the HTTP 200 the transport returns for a failed call.
102
+ *
103
+ * NEVER hand this the arguments or the result body: they carry user app
104
+ * content. The report is deliberately just (tool, outcome, code, duration).
105
+ */
106
+ onToolResult?: (report: ToolCallReport) => void;
54
107
  }
55
108
  /** One registered tool: name, human/LLM description, Zod input shape, handler. */
56
109
  export interface ToolDef {
package/dist/tools.js CHANGED
@@ -30,6 +30,89 @@ import { readFileSync, writeFileSync } from "node:fs";
30
30
  import { basename } from "node:path";
31
31
  import { resolveUrl, describeActiveConfig, clearActiveProfile, } from "./config.js";
32
32
  import { fetchSkill } from "./skill.js";
33
+ /**
34
+ * Where errorResult()/invalidArgs() stash the STRUCTURED error code they
35
+ * already computed, so a host can report it without re-parsing the serialized
36
+ * JSON text back out of `content[0].text` (issue #1287).
37
+ *
38
+ * A Symbol, set non-enumerable, on purpose: it is invisible to
39
+ * JSON.stringify, to `{...result}`, to Object.keys, and to the MCP SDK's Zod
40
+ * passthrough copy — so tagging cannot change a single byte on the wire. Only
41
+ * runTool() below (and toolErrorCode(), for tests) ever reads it.
42
+ *
43
+ * Tagging the returned OBJECT rather than threading `env` through all 100-odd
44
+ * errorResult()/invalidArgs() call sites keeps this a ~10-line change, and it
45
+ * attributes the code to the result actually returned rather than to whatever
46
+ * an async scope happened to see last.
47
+ */
48
+ const TOOL_ERROR_CODE = Symbol("homespun.toolErrorCode");
49
+ /** Tag a result with its structured error code and return it unchanged. */
50
+ function tagErrorCode(result, code) {
51
+ Object.defineProperty(result, TOOL_ERROR_CODE, {
52
+ value: code,
53
+ enumerable: false,
54
+ writable: false,
55
+ configurable: true,
56
+ });
57
+ return result;
58
+ }
59
+ /**
60
+ * Read the structured error code off a ToolResult produced by
61
+ * errorResult()/invalidArgs(). Undefined for a success, and for an `isError`
62
+ * result built by hand somewhere else.
63
+ */
64
+ export function toolErrorCode(result) {
65
+ const code = result[TOOL_ERROR_CODE];
66
+ return typeof code === "string" ? code : undefined;
67
+ }
68
+ /**
69
+ * Invoke a tool handler and report its outcome to `env.onToolResult`.
70
+ *
71
+ * The seam that makes remote MCP calls observable (issue #1287): a tool
72
+ * failure is an `isError` result inside an HTTP 200, so a host that just
73
+ * awaits `tool.handler(...)` cannot tell a failed deploy_app from a successful
74
+ * one. This times the call, catches a throw, and reads the structured error
75
+ * code off the returned result.
76
+ *
77
+ * Transport-agnostic by construction: `onToolResult` is optional, so the stdio
78
+ * CLI server can keep calling `tool.handler` directly (or adopt this and opt
79
+ * in later) with no behaviour change. The handler's result — or its thrown
80
+ * error — is passed through untouched.
81
+ */
82
+ export async function runTool(tool, client, args, env) {
83
+ const started = Date.now();
84
+ let result;
85
+ try {
86
+ result = await tool.handler(client, args, env);
87
+ }
88
+ catch (e) {
89
+ report(env, {
90
+ tool: tool.name,
91
+ outcome: "exception",
92
+ errorCode: "internal",
93
+ ms: Date.now() - started,
94
+ });
95
+ throw e;
96
+ }
97
+ report(env, {
98
+ tool: tool.name,
99
+ outcome: result.isError === true ? "error" : "ok",
100
+ errorCode: result.isError === true ? toolErrorCode(result) : undefined,
101
+ ms: Date.now() - started,
102
+ });
103
+ return result;
104
+ }
105
+ /** Fire the reporting callback; telemetry must never break a tool call. */
106
+ function report(env, r) {
107
+ if (!env?.onToolResult)
108
+ return;
109
+ try {
110
+ env.onToolResult(r);
111
+ }
112
+ catch {
113
+ // Swallow: a broken observability hook must not fail the tool.
114
+ }
115
+ }
33
116
  /** Wrap a JSON-able value as a single text-content tool result. */
34
117
  function jsonResult(value) {
35
118
  return {
@@ -59,13 +142,13 @@ function errorResult(e) {
59
142
  payload["details"] = e.details;
60
143
  if (e.retryable !== undefined)
61
144
  payload["retryable"] = e.retryable;
62
- return {
145
+ return tagErrorCode({
63
146
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
64
147
  isError: true,
65
- };
148
+ }, e.code);
66
149
  }
67
150
  const message = e instanceof Error ? e.message : String(e);
68
- return {
151
+ return tagErrorCode({
69
152
  content: [
70
153
  {
71
154
  type: "text",
@@ -73,14 +156,14 @@ function errorResult(e) {
73
156
  },
74
157
  ],
75
158
  isError: true,
76
- };
159
+ }, "internal");
77
160
  }
78
161
  /**
79
162
  * Structured invalid_args error for the per-action validation inside
80
163
  * consolidated tools. Mirrors the relay's envelope so the model self-corrects.
81
164
  */
82
165
  function invalidArgs(message) {
83
- return {
166
+ return tagErrorCode({
84
167
  content: [
85
168
  {
86
169
  type: "text",
@@ -88,7 +171,7 @@ function invalidArgs(message) {
88
171
  },
89
172
  ],
90
173
  isError: true,
91
- };
174
+ }, "invalid_args");
92
175
  }
93
176
  /** Read a required string arg; returns undefined when absent/empty. */
94
177
  function str(args, key) {
@@ -277,6 +360,24 @@ const deleteRowShape = {
277
360
  .optional()
278
361
  .describe("Optional optimistic-lock version."),
279
362
  };
363
+ const restoreRowShape = {
364
+ app_id: z.string().min(1).describe("The app id."),
365
+ collection: z.string().min(1).describe("The collection name."),
366
+ key: z.string().min(1).describe("The key of the deleted row to restore."),
367
+ };
368
+ const listDeletedRowsShape = {
369
+ app_id: z.string().min(1).describe("The app id."),
370
+ collection: z.string().min(1).describe("The collection name."),
371
+ limit: z
372
+ .number()
373
+ .int()
374
+ .optional()
375
+ .describe("Max rows to return (default 100)."),
376
+ before: z
377
+ .string()
378
+ .optional()
379
+ .describe("Cursor for the next page: pass back the previous page's next_before."),
380
+ };
280
381
  const getFeedEventsShape = {
281
382
  app_id: z.string().min(1).describe("The app id."),
282
383
  since: z
@@ -569,6 +670,7 @@ const communityShape = {
569
670
  action: z
570
671
  .enum([
571
672
  "publish",
673
+ "unpublish",
572
674
  "get_config_contract",
573
675
  "install",
574
676
  "list_pending",
@@ -577,7 +679,7 @@ const communityShape = {
577
679
  "reject",
578
680
  "set_trust_level",
579
681
  ])
580
- .describe("publish: publish one of YOUR apps as a community template (app_id; optional title/description/category/tags). PRIVACY: 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. Do NOT publish an app whose seedOnInstall collections hold real personal data (names, emails, addresses, messages, anything private): seed data must be example-only. Pass attest_example_only:true to attest you have checked this. 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 (the response's expedited/auto_approved tell you which). 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-upload it with the attachments tool (scope agent) and pass its attachment id. After installing a template with connect_steps, run the `ingest` tool's list action on the new app_id to read its freshly provisioned hook URLs, and wire each into the external service. install: install a template by `ref` for YOU (your owning human becomes the owner). Pass `config` as { stepKey: value } from the contract: a 'config' step's value is a string, an 'upload' step's value is a pre-uploaded attachment id. A required step you omit 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')."),
682
+ .describe("publish: publish one of YOUR apps as a community template (app_id; optional title/description/category/tags). PRIVACY: 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. Do NOT publish an app whose seedOnInstall collections hold real personal data (names, emails, addresses, messages, anything private): seed data must be example-only. Pass attest_example_only:true to attest you have checked this. 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 (the response's expedited/auto_approved tell you which). unpublish: take one of YOUR 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 is not yours reads as not found either way. Publish a new version to put 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-upload it with the attachments tool (scope agent) and pass its attachment id. After installing a template with connect_steps, run the `ingest` tool's list action on the new app_id to read its freshly provisioned hook URLs, and wire each into the external service. install: install a template by `ref` for YOU (your owning human becomes the owner). Pass `config` as { stepKey: value } from the contract: a 'config' step's value is a string, an 'upload' step's value is a pre-uploaded attachment id. A required step you omit 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')."),
581
683
  ref: z
582
684
  .string()
583
685
  .optional()
@@ -674,7 +776,7 @@ const communityShape = {
674
776
  snapshot_id: z
675
777
  .string()
676
778
  .optional()
677
- .describe("Required for get_submission/approve/reject. The submission's snapshot id (from publish's response or list_pending)."),
779
+ .describe("Required for get_submission/unpublish/approve/reject. The submission's snapshot id (from publish's response or list_pending)."),
678
780
  note: z
679
781
  .string()
680
782
  .optional()
@@ -978,7 +1080,7 @@ export const TOOLS = [
978
1080
  },
979
1081
  {
980
1082
  name: "delete_row",
981
- description: "Soft-delete a row from a v2 app's collection. A watcher sees the deletion live as op:delete on the change feed. Pass if_match for an optimistic-locked delete. Returns { deleted: true }.",
1083
+ description: "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 }.",
982
1084
  inputSchema: deleteRowShape,
983
1085
  annotations: {
984
1086
  title: "Delete Row",
@@ -1000,6 +1102,53 @@ export const TOOLS = [
1000
1102
  }
1001
1103
  },
1002
1104
  },
1105
+ {
1106
+ name: "list_deleted_rows",
1107
+ description: "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 }.",
1108
+ inputSchema: listDeletedRowsShape,
1109
+ annotations: {
1110
+ title: "List Deleted Rows",
1111
+ readOnlyHint: true,
1112
+ destructiveHint: false,
1113
+ idempotentHint: true,
1114
+ openWorldHint: false,
1115
+ },
1116
+ handler: async (client, args) => {
1117
+ try {
1118
+ const opts = {};
1119
+ if (args["limit"] !== undefined)
1120
+ opts.limit = args["limit"];
1121
+ if (args["before"] !== undefined)
1122
+ opts.before = String(args["before"]);
1123
+ return jsonResult(await client.listDeletedAppRows(String(args["app_id"]), String(args["collection"]), opts));
1124
+ }
1125
+ catch (e) {
1126
+ return errorResult(e);
1127
+ }
1128
+ },
1129
+ },
1130
+ {
1131
+ name: "restore_row",
1132
+ description: "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 }.",
1133
+ inputSchema: restoreRowShape,
1134
+ annotations: {
1135
+ title: "Restore Row",
1136
+ readOnlyHint: false,
1137
+ // Brings a row BACK. It writes, but it only ever adds; nothing is
1138
+ // removed or overwritten, which is the opposite of destructive.
1139
+ destructiveHint: false,
1140
+ idempotentHint: false,
1141
+ openWorldHint: false,
1142
+ },
1143
+ handler: async (client, args) => {
1144
+ try {
1145
+ return jsonResult(await client.restoreAppRow(String(args["app_id"]), String(args["collection"]), String(args["key"])));
1146
+ }
1147
+ catch (e) {
1148
+ return errorResult(e);
1149
+ }
1150
+ },
1151
+ },
1003
1152
  {
1004
1153
  name: "get_feed_events",
1005
1154
  description: "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 }.",
@@ -1699,17 +1848,20 @@ export const TOOLS = [
1699
1848
  },
1700
1849
  {
1701
1850
  name: "community",
1702
- description: "Publishing an app as a community template, installing a template, and, for relay operators, reviewing submissions. Actions: publish, get_config_contract, install, list_pending, get_submission, approve, reject, set_trust_level.\n\npublish 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.\n\nget_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.\n\nThe 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.",
1851
+ description: "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.\n\npublish 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.\n\nunpublish 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.\n\nget_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.\n\nThe 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.",
1703
1852
  inputSchema: communityShape,
1704
1853
  // Consolidated tool: read actions (list_pending/get_submission) + mutating
1705
- // ones (publish/approve/reject). Hint reflects the most-privileged action.
1854
+ // ones (publish/unpublish/approve/reject). Hint reflects the
1855
+ // most-privileged action.
1706
1856
  annotations: {
1707
1857
  title: "Community Templates",
1708
1858
  readOnlyHint: false,
1709
- // publish | install | the operator review actions. `approve`, `reject`
1710
- // and `set_trust_level` move a submission between states; the
1711
- // submission itself survives every one of them.
1712
- destructiveHint: false,
1859
+ // `approve`, `reject` and `set_trust_level` only move a submission
1860
+ // between states and the submission itself survives every one of them,
1861
+ // but `unpublish` REMOVES a live listing from the public gallery, from
1862
+ // search, and from its direct install link. That is existing state
1863
+ // going away, so the tool is destructive.
1864
+ destructiveHint: true,
1713
1865
  idempotentHint: false,
1714
1866
  openWorldHint: true,
1715
1867
  },
@@ -1740,6 +1892,11 @@ export const TOOLS = [
1740
1892
  attestExampleOnly: bool(args, "attest_example_only"),
1741
1893
  }));
1742
1894
  }
1895
+ case "unpublish":
1896
+ if (str(args, "snapshot_id") === undefined) {
1897
+ return invalidArgs("unpublish requires `snapshot_id`");
1898
+ }
1899
+ return jsonResult(await client.unpublishCommunityTemplate(String(args["snapshot_id"])));
1743
1900
  case "get_config_contract": {
1744
1901
  const ref = str(args, "ref");
1745
1902
  if (ref === undefined) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.6.38";
1
+ export declare const VERSION = "1.6.39";
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.38";
3
+ export const VERSION = "1.6.39";
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.38",
4
+ "version": "1.6.39",
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",
@@ -45,14 +45,14 @@
45
45
  "test:unit": "vitest run"
46
46
  },
47
47
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.20.0",
49
- "@homespunapps/core": "^1.6.38",
48
+ "@modelcontextprotocol/sdk": "^1.30.0",
49
+ "@homespunapps/core": "^1.6.39",
50
50
  "zod": "^4.4.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^26.1.1",
53
+ "@types/node": "^26.1.2",
54
54
  "typescript": "^7.0.2",
55
- "vitest": "^4.1.8"
55
+ "vitest": "^4.1.10"
56
56
  },
57
57
  "repository": {
58
58
  "type": "git",
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.38",
6
+ "version": "1.6.39",
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.38",
13
+ "version": "1.6.39",
14
14
  "transport": {
15
15
  "type": "stdio"
16
16
  },