@homespunapps/mcp 1.6.37 → 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 +1 -1
- package/dist/tools.d.ts +53 -0
- package/dist/tools.js +172 -15
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/server.json +2 -2
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
|
|
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
|
-
//
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
|
|
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.
|
|
1
|
+
export declare const VERSION = "1.6.39";
|
package/dist/version.js
CHANGED
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.
|
|
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.
|
|
49
|
-
"@homespunapps/core": "^1.6.
|
|
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.
|
|
53
|
+
"@types/node": "^26.1.2",
|
|
54
54
|
"typescript": "^7.0.2",
|
|
55
|
-
"vitest": "^4.1.
|
|
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.
|
|
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.
|
|
13
|
+
"version": "1.6.39",
|
|
14
14
|
"transport": {
|
|
15
15
|
"type": "stdio"
|
|
16
16
|
},
|