@homespunapps/mcp 1.6.78 → 1.6.80
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 +108 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -179,6 +179,29 @@ function invalidArgs(message) {
|
|
|
179
179
|
isError: true,
|
|
180
180
|
}, "invalid_args");
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* The `transfer` tool talks to /v1/apps/:id/transfer through
|
|
184
|
+
* HomespunClient.call(), the one low-level primitive it exposes publicly,
|
|
185
|
+
* because that route has no typed wrapper yet. asRelayObject()/failRelay()
|
|
186
|
+
* mirror the private asObject()/fail() pair every typed wrapper method uses
|
|
187
|
+
* internally, so a raw call() still produces the same HomespunApiError shape
|
|
188
|
+
* errorResult() already knows how to report.
|
|
189
|
+
*/
|
|
190
|
+
function asRelayObject(r) {
|
|
191
|
+
if (r.data === null || typeof r.data !== "object" || Array.isArray(r.data)) {
|
|
192
|
+
throw new HomespunApiError(r.status, "invalid_response", `relay returned a ${r.status} with a non-object body`, { body: r.data });
|
|
193
|
+
}
|
|
194
|
+
return r.data;
|
|
195
|
+
}
|
|
196
|
+
/** Throw a HomespunApiError from a failed RelayResponse (see asRelayObject). */
|
|
197
|
+
function failRelay(r) {
|
|
198
|
+
const err = r.data?.error;
|
|
199
|
+
throw new HomespunApiError(r.status, err?.code ?? "relay_error", err?.message ?? `relay returned ${r.status}`, err?.details, {
|
|
200
|
+
hint: err?.hint,
|
|
201
|
+
retryable: err?.retryable,
|
|
202
|
+
docsUrl: err?.docs_url,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
182
205
|
/** Read a required string arg; returns undefined when absent/empty. */
|
|
183
206
|
function str(args, key) {
|
|
184
207
|
const v = args[key];
|
|
@@ -553,6 +576,20 @@ const grantsShape = {
|
|
|
553
576
|
.optional()
|
|
554
577
|
.describe("revoke only. The grant link id (see list's `id` field)."),
|
|
555
578
|
};
|
|
579
|
+
const transferShape = {
|
|
580
|
+
action: z
|
|
581
|
+
.enum(["start", "status", "cancel"])
|
|
582
|
+
.describe("A v2 app's ownership transfer (issue #1847). start does not move ownership: it only mints a pending offer and emails the named person an accept link, and the app stays owned here until they open it and accept (app_id+email; optional keep_as_member). status: the app's pending transfer, or null if none is pending (app_id). cancel: withdraw a pending transfer; idempotent, so cancelling with none pending is still a success (app_id)."),
|
|
583
|
+
app_id: z.string().min(1).describe("The app id."),
|
|
584
|
+
email: z
|
|
585
|
+
.string()
|
|
586
|
+
.optional()
|
|
587
|
+
.describe("start only. The email to offer ownership to. The relay sends that address an accept link; ownership moves only when they open it and accept, never at start itself. 409s if a transfer is already pending for this app, or if the email already owns it."),
|
|
588
|
+
keep_as_member: z
|
|
589
|
+
.boolean()
|
|
590
|
+
.optional()
|
|
591
|
+
.describe("start only. Whether the current owner stays on as an ordinary member once the transfer is accepted, losing owner powers but keeping app access. Defaults to true. Has no effect unless and until the transfer is actually accepted."),
|
|
592
|
+
};
|
|
556
593
|
const credentialsShape = {
|
|
557
594
|
action: z
|
|
558
595
|
.enum(["mint", "list", "pause", "resume", "rotate", "revoke"])
|
|
@@ -1643,6 +1680,77 @@ export const TOOLS = [
|
|
|
1643
1680
|
}
|
|
1644
1681
|
},
|
|
1645
1682
|
},
|
|
1683
|
+
{
|
|
1684
|
+
name: "transfer",
|
|
1685
|
+
description: "A v2 app's ownership transfer (issue #1847): handing the app, and the quota and billing responsibility that come with it, to a different human. This is two steps, and `start` completing is only the first one. start mints a pending offer and emails the named person an accept link; the app is still owned here when the call returns, stays owned here while the offer is pending, and only moves once that person opens the link and accepts it. Reporting the transfer as done after `start` would be wrong: check `status` to see whether it is still pending or has dropped to null (accepted, expired, or withdrawn), and the caller's own agent key stops being able to deploy this app only at the moment it actually moves. Actions: start offers the app to an email address, 409ing if a transfer is already pending or the email already owns the app; status returns the pending transfer, or null if none; cancel withdraws a pending transfer and is idempotent, so cancelling with nothing pending is still a success.",
|
|
1686
|
+
inputSchema: transferShape,
|
|
1687
|
+
// Consolidated tool: read action (status) + mutating ones (start/cancel).
|
|
1688
|
+
// Hint reflects start, the most-privileged action: it hands the app to
|
|
1689
|
+
// someone else once accepted.
|
|
1690
|
+
annotations: {
|
|
1691
|
+
title: "Manage App Ownership Transfer",
|
|
1692
|
+
readOnlyHint: false,
|
|
1693
|
+
// Destructive in effect once accepted: `start` sets an app on a path to
|
|
1694
|
+
// a different owner, and `cancel` withdraws that path.
|
|
1695
|
+
destructiveHint: true,
|
|
1696
|
+
// NOT idempotent: `start` mints a fresh pending offer, and a retry
|
|
1697
|
+
// while one is already pending 409s rather than silently having no
|
|
1698
|
+
// further effect. Matches the `grants` tool, which is the same shape.
|
|
1699
|
+
idempotentHint: false,
|
|
1700
|
+
openWorldHint: false,
|
|
1701
|
+
},
|
|
1702
|
+
handler: async (client, args) => {
|
|
1703
|
+
const action = String(args["action"]);
|
|
1704
|
+
if (str(args, "app_id") === undefined) {
|
|
1705
|
+
return invalidArgs(`${action} requires \`app_id\``);
|
|
1706
|
+
}
|
|
1707
|
+
const appId = String(args["app_id"]);
|
|
1708
|
+
const path = `/v1/apps/${encodeURIComponent(appId)}/transfer`;
|
|
1709
|
+
try {
|
|
1710
|
+
switch (action) {
|
|
1711
|
+
case "start": {
|
|
1712
|
+
if (str(args, "email") === undefined) {
|
|
1713
|
+
return invalidArgs("start requires `email`");
|
|
1714
|
+
}
|
|
1715
|
+
const keepAsMember = bool(args, "keep_as_member");
|
|
1716
|
+
const r = await client.call("POST", path, {
|
|
1717
|
+
email: String(args["email"]),
|
|
1718
|
+
...(keepAsMember !== undefined ? { keepAsMember } : {}),
|
|
1719
|
+
});
|
|
1720
|
+
if (!r.ok)
|
|
1721
|
+
failRelay(r);
|
|
1722
|
+
const started = asRelayObject(r);
|
|
1723
|
+
return jsonResult({
|
|
1724
|
+
...started,
|
|
1725
|
+
// See the tool description: `start` succeeding only means the
|
|
1726
|
+
// offer was sent, not that ownership moved. Stated again here,
|
|
1727
|
+
// in the result itself, so a caller reading just this response
|
|
1728
|
+
// cannot mistake it for a completed transfer.
|
|
1729
|
+
ownership_moved: false,
|
|
1730
|
+
note: "Ownership has not moved. It only moves once the recipient opens the emailed link and accepts; poll `status` to see when that happens.",
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
case "status": {
|
|
1734
|
+
const r = await client.call("GET", path);
|
|
1735
|
+
if (!r.ok)
|
|
1736
|
+
failRelay(r);
|
|
1737
|
+
return jsonResult(asRelayObject(r));
|
|
1738
|
+
}
|
|
1739
|
+
case "cancel": {
|
|
1740
|
+
const r = await client.call("DELETE", path);
|
|
1741
|
+
if (!r.ok)
|
|
1742
|
+
failRelay(r);
|
|
1743
|
+
return jsonResult({ app_id: appId, cancelled: true });
|
|
1744
|
+
}
|
|
1745
|
+
default:
|
|
1746
|
+
return invalidArgs(`unknown transfer action '${action}'`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
catch (e) {
|
|
1750
|
+
return errorResult(e);
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1646
1754
|
// ----- consolidated management tools --------------------------------------
|
|
1647
1755
|
{
|
|
1648
1756
|
name: "credentials",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.6.
|
|
1
|
+
export declare const VERSION = "1.6.80";
|
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.80",
|
|
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.30.0",
|
|
49
|
-
"@homespunapps/core": "^1.6.
|
|
49
|
+
"@homespunapps/core": "^1.6.80",
|
|
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.
|
|
6
|
+
"version": "1.6.80",
|
|
7
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.
|
|
13
|
+
"version": "1.6.80",
|
|
14
14
|
"transport": {
|
|
15
15
|
"type": "stdio"
|
|
16
16
|
},
|