@homespunapps/mcp 1.6.0 → 1.6.2
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.js +48 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +9 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @homespunapps/mcp
|
|
2
2
|
|
|
3
|
-
A thin **stdio [Model Context Protocol](https://modelcontextprotocol.io) server** for [Homespun](https://homespun.dev). It lets any MCP client (Claude Desktop, Cursor, Windsurf, Cline, your own host)
|
|
3
|
+
A thin **stdio [Model Context Protocol](https://modelcontextprotocol.io) server** for [Homespun](https://homespun.dev). It lets any MCP client (Claude Desktop, Cursor, Windsurf, Cline, your own host) deploy a real multi-user web app in one call and keep operating its data afterwards: hosting on its own URL, magic-link identity, a shared realtime database with per-role permissions, email notifications and file attachments, all included.
|
|
4
4
|
|
|
5
5
|
It is a wrapper, not a reimplementation: all relay I/O goes through [`@homespunapps/core`](https://www.npmjs.com/package/@homespunapps/core), and config is shared with the [`homespun` CLI](https://www.npmjs.com/package/@homespunapps/cli) (`~/.config/homespun/config.json`) — so the CLI and this server use the **same agent identity**.
|
|
6
6
|
|
package/dist/tools.js
CHANGED
|
@@ -186,20 +186,30 @@ const deployAppShape = {
|
|
|
186
186
|
.optional()
|
|
187
187
|
.describe("REDEPLOY only. Bypass the compat gate on a narrowing manifest change (a removed/narrowed collection is detached, never deleted)."),
|
|
188
188
|
assets: z
|
|
189
|
-
.array(z.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
.
|
|
189
|
+
.array(z.union([
|
|
190
|
+
z.object({
|
|
191
|
+
path: z
|
|
192
|
+
.string()
|
|
193
|
+
.describe("App-relative, same-origin reference the HTML uses, e.g. 'frames/000.jpg' or 'media/intro.mp4'. Relative ONLY: no leading '/', no '..' segment, no backslash, charset [A-Za-z0-9._/-], not under a reserved prefix (_hs, b)."),
|
|
194
|
+
content_base64: z
|
|
195
|
+
.string()
|
|
196
|
+
.describe("Standard base64 of the asset's raw bytes."),
|
|
197
|
+
mime: z
|
|
198
|
+
.string()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe("Advisory content-type. The relay sniffs the REAL type from the bytes and enforces the attachment allowlist; omit it (or set application/octet-stream) for data files like CSV that don't magic-byte-sniff, so they are stored + served as an inert download."),
|
|
201
|
+
}),
|
|
202
|
+
z.object({
|
|
203
|
+
path: z
|
|
204
|
+
.string()
|
|
205
|
+
.describe("App-relative, same-origin reference the HTML uses (same rules as the inline form)."),
|
|
206
|
+
attachment_id: z
|
|
207
|
+
.string()
|
|
208
|
+
.describe("Id of an ALREADY-uploaded attachment to bind at this path, instead of carrying base64. Use with `attachments fetch` (URL, zero model-context cost) or presign + finalize: the attachment must be owned by YOU, app-scoped to THIS app (upload it with scope=app, app_id=this app), and ready. Skips decode/upload; the deploy just maps path -> attachment_id."),
|
|
209
|
+
}),
|
|
210
|
+
]))
|
|
211
|
+
.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."),
|
|
203
213
|
};
|
|
204
214
|
const listRowsShape = {
|
|
205
215
|
app_id: z.string().min(1).describe("The app id."),
|
|
@@ -413,6 +423,7 @@ const attachmentsShape = {
|
|
|
413
423
|
action: z
|
|
414
424
|
.enum([
|
|
415
425
|
"upload",
|
|
426
|
+
"fetch",
|
|
416
427
|
"presign",
|
|
417
428
|
"finalize",
|
|
418
429
|
"download",
|
|
@@ -423,7 +434,7 @@ const attachmentsShape = {
|
|
|
423
434
|
"revoke_token",
|
|
424
435
|
"list_tokens",
|
|
425
436
|
])
|
|
426
|
-
.describe("Binary attachment operations.
|
|
437
|
+
.describe("Binary attachment operations. For ANY real image or media (anything beyond a tiny icon) PREFER a ZERO-CONTEXT path so the bytes never enter the model context and cost NO tokens: `fetch` when you have a URL (the relay downloads it server-side; you send only the URL string), or presign + finalize when the client can PUT the raw bytes out-of-band. upload with `content_base64` sends the bytes INLINE in the tool-call arguments, loading them into the model context at a token cost PROPORTIONAL TO FILE SIZE (even a few-hundred-KB image is very costly, worse on every retry); use it only as a last-resort fallback for small assets or clients that have neither a URL nor an out-of-band PUT. fetch: pass { source_url, scope } and the relay fetches the bytes itself (https only, SSRF-guarded) and runs the same sniff/allowlist/size/quota/scan checks as any upload. upload: `content_base64` (base64 bytes, no filesystem) or `file_path` (absolute, read on the RELAY host); scope agent|app. presign + finalize: (1) presign with { mime, size, sha256, scope }, (2) PUT the bytes to put_url out-of-band, (3) finalize confirms it (re-sniffs + re-checks the bytes). download: fetch bytes by attachment_id to out_path (absolute) or return base64. show: metadata only. list: the agent's attachments. delete: soft-delete. mint_token: mint a /b/<token> capability URL (returned ONCE). revoke_token / list_tokens: manage those tokens."),
|
|
427
438
|
size: z
|
|
428
439
|
.number()
|
|
429
440
|
.int()
|
|
@@ -442,6 +453,10 @@ const attachmentsShape = {
|
|
|
442
453
|
.string()
|
|
443
454
|
.optional()
|
|
444
455
|
.describe("upload: ABSOLUTE path to a file read on the SERVER host running this MCP connector (the relay), NOT your machine. Only works when the file is local to the relay (e.g. a locally-run CLI). For a hosted or remote agent, use `content_base64` instead."),
|
|
456
|
+
source_url: z
|
|
457
|
+
.string()
|
|
458
|
+
.optional()
|
|
459
|
+
.describe("fetch: an https URL the RELAY downloads server-side, so the bytes NEVER enter the model context (zero token cost). SSRF-guarded: https only, no private/loopback/link-local/metadata hosts, DNS pinned, redirects refused, size-capped, timed out. The downloaded bytes run the same byte-sniff/allowlist/size/quota/scan checks as any upload. Prefer this (or presign+finalize) over `content_base64` for real images/media."),
|
|
445
460
|
content_base64: z
|
|
446
461
|
.string()
|
|
447
462
|
.optional()
|
|
@@ -1285,7 +1300,7 @@ export const TOOLS = [
|
|
|
1285
1300
|
// ----- consolidated management tools --------------------------------------
|
|
1286
1301
|
{
|
|
1287
1302
|
name: "attachments",
|
|
1288
|
-
description: "Binary attachments (images, PDFs, audio, video) referenced from event payloads / input_data via `format: homespun-attachment-id`. ONE tool with an `action` enum: upload | presign | finalize | download | show | list | delete | mint_token | revoke_token | list_tokens. TOKEN COST, READ FIRST: an inline `upload` with `content_base64` carries the bytes in the tool-call arguments, so they enter the MODEL CONTEXT and cost tokens PROPORTIONAL TO FILE SIZE (a few-hundred-KB image is already very costly, worse on every retry).
|
|
1303
|
+
description: "Binary attachments (images, PDFs, audio, video) referenced from event payloads / input_data via `format: homespun-attachment-id`. ONE tool with an `action` enum: upload | fetch | presign | finalize | download | show | list | delete | mint_token | revoke_token | list_tokens. TOKEN COST, READ FIRST: an inline `upload` with `content_base64` carries the bytes in the tool-call arguments, so they enter the MODEL CONTEXT and cost tokens PROPORTIONAL TO FILE SIZE (a few-hundred-KB image is already very costly, worse on every retry). For ANY real image or media (anything beyond a tiny icon) use a ZERO-CONTEXT path instead: `fetch` when you have a URL (the relay downloads it server-side, you send only the URL string), or presign + finalize when the client can PUT the raw bytes out-of-band. fetch: { source_url (https), scope } — the relay downloads the URL itself behind an SSRF guard (https only, no private/loopback/metadata hosts, DNS pinned, redirects refused, size-capped, timed out) and runs the same byte-sniff + allowlist + size + quota + scan checks as any upload; works on any storage backend. upload (inline) takes EITHER `content_base64` (base64 bytes, no filesystem; last-resort for small assets or clients with neither a URL nor an out-of-band PUT) OR `file_path` (ABSOLUTE path read on the RELAY host, only usable when the file is local to the relay). presign + finalize (token-free, for images/video/big audio): (1) presign with { mime, size, sha256, scope } returns { put_url, attachment_id }; (2) YOU PUT the raw bytes to put_url over plain HTTP out-of-band; (3) finalize with the attachment_id. At finalize the relay re-reads the stored bytes, BYTE-SNIFFS the real type, and enforces the same allowlist + size + sha256 + quota + scan checks as any upload, so a presign that lies about 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 (use fetch or inline upload there). download writes to an ABSOLUTE out_path (or returns base64). Scope an upload to agent (default, reusable) or app. mint_token returns a /b/<token> capability URL (ONCE) a browser can GET without your API key.",
|
|
1289
1304
|
inputSchema: attachmentsShape,
|
|
1290
1305
|
// Consolidated tool: read actions (download/show/list/list_tokens) +
|
|
1291
1306
|
// mutating ones (upload/delete/mint_token/revoke_token). openWorld:true
|
|
@@ -1347,6 +1362,23 @@ export const TOOLS = [
|
|
|
1347
1362
|
});
|
|
1348
1363
|
return jsonResult(ref);
|
|
1349
1364
|
}
|
|
1365
|
+
case "fetch": {
|
|
1366
|
+
// Server-side URL ingestion: the relay downloads source_url itself
|
|
1367
|
+
// (SSRF-guarded), so no bytes ride in the tool-call arguments and
|
|
1368
|
+
// nothing enters the model context.
|
|
1369
|
+
const sourceUrl = str(args, "source_url");
|
|
1370
|
+
if (sourceUrl === undefined)
|
|
1371
|
+
return invalidArgs("fetch requires `source_url` (an https URL the relay downloads server-side)");
|
|
1372
|
+
const scope = (str(args, "scope") ?? "agent");
|
|
1373
|
+
if (scope === "app" && str(args, "app_id") === undefined)
|
|
1374
|
+
return invalidArgs("scope=app requires `app_id`");
|
|
1375
|
+
const ref = await client.fetchBlob(sourceUrl, {
|
|
1376
|
+
scope,
|
|
1377
|
+
appId: str(args, "app_id"),
|
|
1378
|
+
mime: str(args, "mime"),
|
|
1379
|
+
});
|
|
1380
|
+
return jsonResult(ref);
|
|
1381
|
+
}
|
|
1350
1382
|
case "presign": {
|
|
1351
1383
|
// Large-file direct-to-storage: reserve a pending attachment + get a
|
|
1352
1384
|
// PUT URL. The caller PUTs the bytes to put_url over HTTP, then calls
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.6.
|
|
1
|
+
export declare const VERSION = "1.6.2";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homespunapps/mcp",
|
|
3
3
|
"mcpName": "dev.homespun/homespun",
|
|
4
|
-
"version": "1.6.
|
|
5
|
-
"description": "Model Context Protocol (stdio) server for Homespun: lets any MCP client (Claude Desktop, Cursor, …)
|
|
4
|
+
"version": "1.6.2",
|
|
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",
|
|
8
8
|
"keywords": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
49
|
-
"@homespunapps/core": "^1.6.
|
|
49
|
+
"@homespunapps/core": "^1.6.2",
|
|
50
50
|
"zod": "^4.4.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "dev.homespun/homespun",
|
|
4
4
|
"title": "Homespun",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "1.6.
|
|
5
|
+
"description": "Deploy a multi-user web app from your agent: hosting, auth, database, and permissions.",
|
|
6
|
+
"version": "1.6.2",
|
|
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.2",
|
|
14
14
|
"transport": {
|
|
15
15
|
"type": "stdio"
|
|
16
16
|
},
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
]
|
|
43
43
|
}
|
|
44
44
|
],
|
|
45
|
+
"remotes": [
|
|
46
|
+
{
|
|
47
|
+
"type": "streamable-http",
|
|
48
|
+
"url": "https://homespun.dev/mcp"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
45
51
|
"repository": {
|
|
46
52
|
"url": "https://github.com/homespunapps/homespun",
|
|
47
53
|
"source": "github",
|