@homespunapps/mcp 1.0.0 → 1.0.1
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 +2 -2
- package/dist/index.js +1 -1
- package/dist/tools.js +247 -13
- package/package.json +3 -8
- package/server.json +2 -5
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://
|
|
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) hand a human a rich interactive UI by URL and get structured data back: forms, approvals, pickers, surveys, dashboards, diff/doc review, multi-step wizards.
|
|
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
|
|
|
@@ -84,7 +84,7 @@ All environment variables are optional — the defaults target the hosted relay
|
|
|
84
84
|
|
|
85
85
|
| Variable | Default | Purpose |
|
|
86
86
|
| --- | --- | --- |
|
|
87
|
-
| `HOMESPUN_URL` | `https://homespun.dev` | Relay base URL. Set to point at a
|
|
87
|
+
| `HOMESPUN_URL` | `https://homespun.dev` | Relay base URL. Set to point at a different relay. |
|
|
88
88
|
| `HOMESPUN_API_KEY` | _(auto-registered)_ | Agent API key. If unset, the server registers an agent on first use and saves the key to `~/.config/homespun/config.json` (shared with the CLI). |
|
|
89
89
|
| `HOMESPUN_TOKEN` | — | Alias for `HOMESPUN_API_KEY` (for hosts that name secrets `*_TOKEN`). `HOMESPUN_API_KEY` wins if both are set. |
|
|
90
90
|
| `HOMESPUN_AGENT_NAME` | `homespun-mcp` | Display name for the auto-registered agent. |
|
package/dist/index.js
CHANGED
|
@@ -69,7 +69,7 @@ Tools exposed: deploy_app, list_rows, get_row, upsert_row, update_row,
|
|
|
69
69
|
delete_row, get_feed_events, apps, members, attachments, taste, key,
|
|
70
70
|
feedback, agent, get_skill.
|
|
71
71
|
|
|
72
|
-
See https://
|
|
72
|
+
See https://docs.homespun.dev for docs.
|
|
73
73
|
`;
|
|
74
74
|
main().catch((e) => {
|
|
75
75
|
process.stderr.write(`homespun-mcp: fatal: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
|
package/dist/tools.js
CHANGED
|
@@ -157,7 +157,7 @@ const deployAppShape = {
|
|
|
157
157
|
.string()
|
|
158
158
|
.min(1)
|
|
159
159
|
.describe("The app's UI as a complete HTML document (single file, up to the relay's size cap)."),
|
|
160
|
-
manifest: jsonObjectSchema.describe("The x-homespun-manifest capability document (a JSON object): app metadata
|
|
160
|
+
manifest: jsonObjectSchema.describe("The x-homespun-manifest capability document (a JSON object). Eight extension keys: app metadata; collections (+ per-collection write/read/delete role lists); 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."),
|
|
161
161
|
visibility: z
|
|
162
162
|
.enum(["private", "link", "public"])
|
|
163
163
|
.optional()
|
|
@@ -170,6 +170,21 @@ const deployAppShape = {
|
|
|
170
170
|
.boolean()
|
|
171
171
|
.optional()
|
|
172
172
|
.describe("REDEPLOY only. Bypass the compat gate on a narrowing manifest change (a removed/narrowed collection is detached, never deleted)."),
|
|
173
|
+
assets: z
|
|
174
|
+
.array(z.object({
|
|
175
|
+
path: z
|
|
176
|
+
.string()
|
|
177
|
+
.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)."),
|
|
178
|
+
content_base64: z
|
|
179
|
+
.string()
|
|
180
|
+
.describe("Standard base64 of the asset's raw bytes."),
|
|
181
|
+
mime: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.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."),
|
|
185
|
+
}))
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("Optional bundle of files shipped WITH the app in ONE deploy: images, fonts, audio/video, data. 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."),
|
|
173
188
|
};
|
|
174
189
|
const listRowsShape = {
|
|
175
190
|
app_id: z.string().min(1).describe("The app id."),
|
|
@@ -248,12 +263,21 @@ const getFeedEventsShape = {
|
|
|
248
263
|
};
|
|
249
264
|
const appsShape = {
|
|
250
265
|
action: z
|
|
251
|
-
.enum([
|
|
252
|
-
|
|
266
|
+
.enum([
|
|
267
|
+
"list",
|
|
268
|
+
"show",
|
|
269
|
+
"update",
|
|
270
|
+
"delete",
|
|
271
|
+
"wake",
|
|
272
|
+
"domain_set",
|
|
273
|
+
"domain_status",
|
|
274
|
+
"domain_remove",
|
|
275
|
+
])
|
|
276
|
+
.describe("list: YOUR owning human's apps. show/update/delete/wake: act on one app (app_id). domain_set/domain_status/domain_remove: manage the app's ONE custom domain (app_id; domain_set also needs domain)."),
|
|
253
277
|
app_id: z
|
|
254
278
|
.string()
|
|
255
279
|
.optional()
|
|
256
|
-
.describe("Required for show/update/delete/wake."),
|
|
280
|
+
.describe("Required for show/update/delete/wake/domain_set/domain_status/domain_remove."),
|
|
257
281
|
status: z
|
|
258
282
|
.enum(["active", "dormant", "archived", "all"])
|
|
259
283
|
.optional()
|
|
@@ -274,6 +298,10 @@ const appsShape = {
|
|
|
274
298
|
.enum(["private", "link", "public"])
|
|
275
299
|
.optional()
|
|
276
300
|
.describe("update only. The new visibility (slug is immutable)."),
|
|
301
|
+
domain: z
|
|
302
|
+
.string()
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("domain_set only. The bare custom domain to bind (e.g. app.example.com). The response's dns_records lists the DNS entries the domain owner must publish."),
|
|
277
305
|
};
|
|
278
306
|
const membersShape = {
|
|
279
307
|
action: z
|
|
@@ -300,6 +328,8 @@ const attachmentsShape = {
|
|
|
300
328
|
action: z
|
|
301
329
|
.enum([
|
|
302
330
|
"upload",
|
|
331
|
+
"presign",
|
|
332
|
+
"finalize",
|
|
303
333
|
"download",
|
|
304
334
|
"show",
|
|
305
335
|
"list",
|
|
@@ -308,7 +338,17 @@ const attachmentsShape = {
|
|
|
308
338
|
"revoke_token",
|
|
309
339
|
"list_tokens",
|
|
310
340
|
])
|
|
311
|
-
.describe("Binary attachment operations. upload:
|
|
341
|
+
.describe("Binary attachment operations. upload: send bytes as an attachment inline; pass `content_base64` (base64 bytes, no filesystem) when you have bytes but no local file (e.g. an image you generated) or you are a hosted/remote agent, or `file_path` (absolute, read on the RELAY host) when the file is local to the relay; scope agent|app. presign + finalize: the LARGE-FILE path (e.g. a video). presign returns a { put_url, attachment_id }, then YOU PUT the raw bytes to put_url over HTTP out-of-band (bytes never pass through this tool / the model context as base64), then finalize confirms it. 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."),
|
|
342
|
+
size: z
|
|
343
|
+
.number()
|
|
344
|
+
.int()
|
|
345
|
+
.positive()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("presign: the exact byte length you will PUT. Committed at presign and re-verified against the uploaded bytes at finalize."),
|
|
348
|
+
sha256: z
|
|
349
|
+
.string()
|
|
350
|
+
.optional()
|
|
351
|
+
.describe("presign: the hex SHA-256 (64 chars) of the exact bytes you will PUT. Committed at presign and re-verified against the uploaded bytes at finalize."),
|
|
312
352
|
attachment_id: z
|
|
313
353
|
.string()
|
|
314
354
|
.optional()
|
|
@@ -316,7 +356,11 @@ const attachmentsShape = {
|
|
|
316
356
|
file_path: z
|
|
317
357
|
.string()
|
|
318
358
|
.optional()
|
|
319
|
-
.describe("upload: ABSOLUTE path to the
|
|
359
|
+
.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."),
|
|
360
|
+
content_base64: z
|
|
361
|
+
.string()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("upload: the file bytes as base64, uploaded WITHOUT any filesystem access. This is the upload path for hosted / remote agents (e.g. an image you just generated): pass it when you have bytes but no file on the relay host. If both `content_base64` and `file_path` are given, `content_base64` wins. The relay sniffs the real type and enforces the same size/allowlist/quota checks as a file upload."),
|
|
320
364
|
scope: z
|
|
321
365
|
.enum(["agent", "app"])
|
|
322
366
|
.optional()
|
|
@@ -329,7 +373,7 @@ const attachmentsShape = {
|
|
|
329
373
|
mime: z
|
|
330
374
|
.string()
|
|
331
375
|
.optional()
|
|
332
|
-
.describe("upload: advisory Content-Type
|
|
376
|
+
.describe("upload/presign: advisory Content-Type. The relay BYTE-SNIFFS the actual bytes and stores/serves that sniffed type regardless (a lying mime is caught, never served inline). Required for presign (scopes the upload URL + fails fast against the allowlist)."),
|
|
333
377
|
out_path: z
|
|
334
378
|
.string()
|
|
335
379
|
.optional()
|
|
@@ -409,6 +453,50 @@ const agentShape = {
|
|
|
409
453
|
.optional()
|
|
410
454
|
.describe("The one-shot claim code (required for claim)."),
|
|
411
455
|
};
|
|
456
|
+
const communityShape = {
|
|
457
|
+
action: z
|
|
458
|
+
.enum(["publish", "list_pending", "get_submission", "approve", "reject"])
|
|
459
|
+
.describe("publish: publish one of YOUR apps as a community template (app_id; optional title/description/category/tags). The capture (html + manifest + the seed rows of seedOnInstall collections) lands PENDING review, installable by its returned direct link but not listed until approved. list_pending / get_submission / approve / reject are RELAY-OPERATOR-only review actions: list_pending (the review queue), get_submission (a submission's full html+manifest+seedRows, 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)."),
|
|
460
|
+
app_id: z
|
|
461
|
+
.string()
|
|
462
|
+
.optional()
|
|
463
|
+
.describe("publish only. The id of an app YOU own to publish."),
|
|
464
|
+
title: z
|
|
465
|
+
.string()
|
|
466
|
+
.optional()
|
|
467
|
+
.describe("publish only. Listing title (1 to 80 chars). Defaults to the app's manifest name."),
|
|
468
|
+
description: z
|
|
469
|
+
.string()
|
|
470
|
+
.optional()
|
|
471
|
+
.describe("publish only. Listing blurb (up to 200 chars). Defaults to the manifest description."),
|
|
472
|
+
category: z
|
|
473
|
+
.string()
|
|
474
|
+
.optional()
|
|
475
|
+
.describe("publish only. Optional single-word category (e.g. 'household')."),
|
|
476
|
+
tags: z
|
|
477
|
+
.array(z.string())
|
|
478
|
+
.optional()
|
|
479
|
+
.describe("publish only. Up to 6 curation tags."),
|
|
480
|
+
snapshot_id: z
|
|
481
|
+
.string()
|
|
482
|
+
.optional()
|
|
483
|
+
.describe("Required for get_submission/approve/reject. The submission's snapshot id (from publish's response or list_pending)."),
|
|
484
|
+
note: z
|
|
485
|
+
.string()
|
|
486
|
+
.optional()
|
|
487
|
+
.describe("reject only. The required rejection note shown to the publisher (delivered to their app feed)."),
|
|
488
|
+
limit: z
|
|
489
|
+
.number()
|
|
490
|
+
.int()
|
|
491
|
+
.positive()
|
|
492
|
+
.max(200)
|
|
493
|
+
.optional()
|
|
494
|
+
.describe("list_pending only. Page size (1..200)."),
|
|
495
|
+
cursor: z
|
|
496
|
+
.string()
|
|
497
|
+
.optional()
|
|
498
|
+
.describe("list_pending only. Opaque cursor from a prior next_cursor."),
|
|
499
|
+
};
|
|
412
500
|
const getSkillShape = {
|
|
413
501
|
version_only: z
|
|
414
502
|
.boolean()
|
|
@@ -422,7 +510,7 @@ export const TOOLS = [
|
|
|
422
510
|
// ----- v2 app lifecycle + data (discrete, hot-path) -----------------------
|
|
423
511
|
{
|
|
424
512
|
name: "deploy_app",
|
|
425
|
-
description:
|
|
513
|
+
description: 'Deploy a v2 app: an HTML document + a capability manifest, hosted at its own URL. The manifest has eight extension keys: app metadata; collections (+ per-collection write/read/delete role lists); 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). Pass EITHER no `app_id` (create, mints a slug + URL) OR `app_id` (redeploy an existing app with new content). A redeploy that NARROWS the manifest (drops a collection, tightens a schema, revokes a role) is refused with manifest_incompatible_redeploy unless force:true; a narrowed collection is then detached, never deleted. Ship images/fonts/audio/video/data FILES with the app in the SAME call via `assets[]`: each is validated + 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 (`<img src="frames/000.jpg">`, `<video src="media/clip.mp4">`), media and font paths support HTTP Range for seeking. A redeploy\'s assets replace the previous version\'s set. BEFORE authoring: call get_skill for the manifest grammar. Returns { app_id, slug, url, version, visibility, created } (create) or { app_id, version, compat, breaks? } (redeploy).',
|
|
426
514
|
inputSchema: deployAppShape,
|
|
427
515
|
annotations: {
|
|
428
516
|
title: "Deploy App",
|
|
@@ -451,6 +539,7 @@ export const TOOLS = [
|
|
|
451
539
|
manifest: manifest.value,
|
|
452
540
|
visibility,
|
|
453
541
|
slug,
|
|
542
|
+
assets: args["assets"],
|
|
454
543
|
}));
|
|
455
544
|
}
|
|
456
545
|
if (str(args, "html") === undefined) {
|
|
@@ -463,6 +552,7 @@ export const TOOLS = [
|
|
|
463
552
|
html: String(args["html"]),
|
|
464
553
|
manifest: manifest.value,
|
|
465
554
|
force: args["force"],
|
|
555
|
+
assets: args["assets"],
|
|
466
556
|
});
|
|
467
557
|
return jsonResult(redeployed);
|
|
468
558
|
}
|
|
@@ -611,7 +701,7 @@ export const TOOLS = [
|
|
|
611
701
|
},
|
|
612
702
|
{
|
|
613
703
|
name: "apps",
|
|
614
|
-
description: "Manage v2 app lifecycle (deploy_app creates/redeploys; this tool covers the rest). ONE tool with an `action` enum: list (YOUR owning human's apps) | show (full detail incl. manifest) | update (visibility only
|
|
704
|
+
description: "Manage v2 app lifecycle (deploy_app creates/redeploys; this tool covers the rest). ONE tool with an `action` enum: list (YOUR owning human's apps) | show (full detail incl. manifest) | update (visibility only - slug is immutable) | delete (soft-delete, idempotent) | wake (a dormant app; a no-op reporting the actual status otherwise) | domain_set (bind ONE custom domain; returns the DNS records the domain owner must publish) | domain_status (the domain record, live-refreshed against Cloudflare when enabled; inspect last_error when it is not activating) | domain_remove (unbind the domain, idempotent).",
|
|
615
705
|
inputSchema: appsShape,
|
|
616
706
|
// Consolidated tool: read actions (list/show) + mutating ones (update/
|
|
617
707
|
// delete/wake). Hint reflects delete, the most-privileged action.
|
|
@@ -662,6 +752,25 @@ export const TOOLS = [
|
|
|
662
752
|
return invalidArgs("wake requires `app_id`");
|
|
663
753
|
}
|
|
664
754
|
return jsonResult(await client.wakeApp(String(args["app_id"])));
|
|
755
|
+
case "domain_set":
|
|
756
|
+
if (str(args, "app_id") === undefined) {
|
|
757
|
+
return invalidArgs("domain_set requires `app_id`");
|
|
758
|
+
}
|
|
759
|
+
if (str(args, "domain") === undefined) {
|
|
760
|
+
return invalidArgs("domain_set requires `domain`");
|
|
761
|
+
}
|
|
762
|
+
return jsonResult(await client.setAppDomain(String(args["app_id"]), String(args["domain"])));
|
|
763
|
+
case "domain_status":
|
|
764
|
+
if (str(args, "app_id") === undefined) {
|
|
765
|
+
return invalidArgs("domain_status requires `app_id`");
|
|
766
|
+
}
|
|
767
|
+
return jsonResult(await client.getAppDomain(String(args["app_id"])));
|
|
768
|
+
case "domain_remove":
|
|
769
|
+
if (str(args, "app_id") === undefined) {
|
|
770
|
+
return invalidArgs("domain_remove requires `app_id`");
|
|
771
|
+
}
|
|
772
|
+
await client.deleteAppDomain(String(args["app_id"]));
|
|
773
|
+
return jsonResult({ app_id: args["app_id"], domain_removed: true });
|
|
665
774
|
default:
|
|
666
775
|
return invalidArgs(`unknown apps action '${action}'`);
|
|
667
776
|
}
|
|
@@ -728,7 +837,7 @@ export const TOOLS = [
|
|
|
728
837
|
// ----- consolidated management tools --------------------------------------
|
|
729
838
|
{
|
|
730
839
|
name: "attachments",
|
|
731
|
-
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 | download | show | list | delete | mint_token | revoke_token | list_tokens. upload
|
|
840
|
+
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. upload (inline) takes EITHER `content_base64` (base64 bytes, no filesystem; use when you generated the bytes in-session or run as a hosted/remote agent) OR `file_path` (ABSOLUTE path read on the RELAY host, only usable when the file is local to the relay). FOR A LARGE FILE (e.g. a video) use presign + finalize instead of base64: (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, so the bytes never route through this tool or the model context, which is why base64 inline is impractical at that size; (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 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.",
|
|
732
841
|
inputSchema: attachmentsShape,
|
|
733
842
|
// Consolidated tool: read actions (download/show/list/list_tokens) +
|
|
734
843
|
// mutating ones (upload/delete/mint_token/revoke_token). openWorld:true
|
|
@@ -746,18 +855,38 @@ export const TOOLS = [
|
|
|
746
855
|
try {
|
|
747
856
|
switch (action) {
|
|
748
857
|
case "upload": {
|
|
858
|
+
// `content_base64` is the documented field; `content` is a silent
|
|
859
|
+
// alias for callers that used the earlier name.
|
|
860
|
+
const contentBase64 = str(args, "content_base64") ?? str(args, "content");
|
|
749
861
|
const filePath = str(args, "file_path");
|
|
750
|
-
if (filePath === undefined)
|
|
751
|
-
return invalidArgs("upload requires `file_path` (
|
|
862
|
+
if (contentBase64 === undefined && filePath === undefined)
|
|
863
|
+
return invalidArgs("upload requires `content_base64` (base64 bytes) or `file_path` (a path local to the relay host)");
|
|
752
864
|
const scope = (str(args, "scope") ?? "agent");
|
|
753
865
|
if (scope === "app" && str(args, "app_id") === undefined)
|
|
754
866
|
return invalidArgs("scope=app requires `app_id`");
|
|
867
|
+
// Inline bytes win when both are given: an explicit `content_base64`
|
|
868
|
+
// is a deliberate no-filesystem upload, so never fall back to reading
|
|
869
|
+
// a file the caller also happened to name. No readFileSync on this
|
|
870
|
+
// path; the base64 is sent straight to the relay's inline route.
|
|
871
|
+
if (contentBase64 !== undefined) {
|
|
872
|
+
const ref = await client.uploadBlobInline(contentBase64, {
|
|
873
|
+
scope,
|
|
874
|
+
appId: str(args, "app_id"),
|
|
875
|
+
filename: str(args, "filename"),
|
|
876
|
+
mime: str(args, "mime"),
|
|
877
|
+
});
|
|
878
|
+
return jsonResult(ref);
|
|
879
|
+
}
|
|
755
880
|
let bytes;
|
|
756
881
|
try {
|
|
757
882
|
bytes = readFileSync(filePath);
|
|
758
883
|
}
|
|
759
884
|
catch (e) {
|
|
760
|
-
|
|
885
|
+
// `file_path` is read on the MCP server / relay host, NOT the
|
|
886
|
+
// calling agent's machine. For a hosted connector that host is
|
|
887
|
+
// Homespun's infra, so a remote agent's path always ENOENTs even
|
|
888
|
+
// when the file exists on its side. Say so, and point at the fix.
|
|
889
|
+
return invalidArgs(`failed to read file_path '${filePath}' (${e instanceof Error ? e.message : String(e)}). Note: file_path is read on the MCP server / relay host, not on your machine, so it only works when the file is local to the relay (e.g. a locally-run CLI). For a hosted or remote agent, pass content_base64 with the file bytes instead.`);
|
|
761
890
|
}
|
|
762
891
|
const ref = await client.uploadBlob(bytes, {
|
|
763
892
|
scope,
|
|
@@ -767,6 +896,43 @@ export const TOOLS = [
|
|
|
767
896
|
});
|
|
768
897
|
return jsonResult(ref);
|
|
769
898
|
}
|
|
899
|
+
case "presign": {
|
|
900
|
+
// Large-file direct-to-storage: reserve a pending attachment + get a
|
|
901
|
+
// PUT URL. The caller PUTs the bytes to put_url over HTTP, then calls
|
|
902
|
+
// finalize. `mime` is advisory (re-sniffed at finalize); size +
|
|
903
|
+
// sha256 are the commitment the finalize re-verifies against the
|
|
904
|
+
// uploaded bytes.
|
|
905
|
+
const mime = str(args, "mime");
|
|
906
|
+
const size = args["size"];
|
|
907
|
+
const sha256 = str(args, "sha256");
|
|
908
|
+
if (mime === undefined ||
|
|
909
|
+
typeof size !== "number" ||
|
|
910
|
+
sha256 === undefined)
|
|
911
|
+
return invalidArgs("presign requires `mime`, `size` (positive integer byte length), and `sha256` (hex sha-256 of the exact bytes you will PUT)");
|
|
912
|
+
const scope = (str(args, "scope") ?? "agent");
|
|
913
|
+
if (scope === "app" && str(args, "app_id") === undefined)
|
|
914
|
+
return invalidArgs("scope=app requires `app_id`");
|
|
915
|
+
const res = await client.presignBlob({
|
|
916
|
+
mime,
|
|
917
|
+
size,
|
|
918
|
+
sha256,
|
|
919
|
+
scope,
|
|
920
|
+
appId: str(args, "app_id"),
|
|
921
|
+
filename: str(args, "filename"),
|
|
922
|
+
});
|
|
923
|
+
// Surface it as { put_url, attachment_id, expires_at }; `put_url`
|
|
924
|
+
// is the name the flow docs use for the out-of-band PUT target.
|
|
925
|
+
return jsonResult({
|
|
926
|
+
put_url: res.upload_url,
|
|
927
|
+
attachment_id: res.attachment_id,
|
|
928
|
+
expires_at: res.expires_at,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
case "finalize": {
|
|
932
|
+
if (str(args, "attachment_id") === undefined)
|
|
933
|
+
return invalidArgs("finalize requires `attachment_id`");
|
|
934
|
+
return jsonResult(await client.finalizeBlob(String(args["attachment_id"])));
|
|
935
|
+
}
|
|
770
936
|
case "download": {
|
|
771
937
|
if (str(args, "attachment_id") === undefined)
|
|
772
938
|
return invalidArgs("download requires `attachment_id`");
|
|
@@ -991,6 +1157,74 @@ export const TOOLS = [
|
|
|
991
1157
|
}
|
|
992
1158
|
},
|
|
993
1159
|
},
|
|
1160
|
+
{
|
|
1161
|
+
name: "community",
|
|
1162
|
+
description: "Publish an app you own as a COMMUNITY TEMPLATE, and (relay operators only) review submissions. ONE tool with an `action` enum: publish | list_pending | get_submission | approve | reject. publish captures your live app (html + manifest + the seed rows of its seedOnInstall collections + listing metadata) into a PENDING template - installable by the returned direct link but NOT listed in the public gallery until an operator approves it; you must have a verified email and at most a few pending submissions at once. The review actions are limited to the relay's configured community reviewers: list_pending (the queue), get_submission (a submission's full content by snapshot_id), approve (list it in the gallery; a re-publish supersedes your app's prior approved version), reject (with a required note that lands in the publisher's app feed).",
|
|
1163
|
+
inputSchema: communityShape,
|
|
1164
|
+
// Consolidated tool: read actions (list_pending/get_submission) + mutating
|
|
1165
|
+
// ones (publish/approve/reject). Hint reflects the most-privileged action.
|
|
1166
|
+
annotations: {
|
|
1167
|
+
title: "Community Templates",
|
|
1168
|
+
readOnlyHint: false,
|
|
1169
|
+
destructiveHint: true,
|
|
1170
|
+
idempotentHint: false,
|
|
1171
|
+
openWorldHint: true,
|
|
1172
|
+
},
|
|
1173
|
+
handler: async (client, args) => {
|
|
1174
|
+
const action = String(args["action"]);
|
|
1175
|
+
try {
|
|
1176
|
+
switch (action) {
|
|
1177
|
+
case "publish": {
|
|
1178
|
+
if (str(args, "app_id") === undefined) {
|
|
1179
|
+
return invalidArgs("publish requires `app_id`");
|
|
1180
|
+
}
|
|
1181
|
+
return jsonResult(await client.publishCommunityTemplate({
|
|
1182
|
+
appId: String(args["app_id"]),
|
|
1183
|
+
title: str(args, "title"),
|
|
1184
|
+
description: str(args, "description"),
|
|
1185
|
+
category: str(args, "category"),
|
|
1186
|
+
tags: Array.isArray(args["tags"])
|
|
1187
|
+
? args["tags"]
|
|
1188
|
+
: undefined,
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
case "list_pending": {
|
|
1192
|
+
const opts = {};
|
|
1193
|
+
if (args["limit"] !== undefined)
|
|
1194
|
+
opts.limit = args["limit"];
|
|
1195
|
+
if (str(args, "cursor") !== undefined)
|
|
1196
|
+
opts.cursor = String(args["cursor"]);
|
|
1197
|
+
return jsonResult(await client.listCommunitySubmissions(opts));
|
|
1198
|
+
}
|
|
1199
|
+
case "get_submission":
|
|
1200
|
+
if (str(args, "snapshot_id") === undefined) {
|
|
1201
|
+
return invalidArgs("get_submission requires `snapshot_id`");
|
|
1202
|
+
}
|
|
1203
|
+
return jsonResult(await client.getCommunitySubmission(String(args["snapshot_id"])));
|
|
1204
|
+
case "approve":
|
|
1205
|
+
if (str(args, "snapshot_id") === undefined) {
|
|
1206
|
+
return invalidArgs("approve requires `snapshot_id`");
|
|
1207
|
+
}
|
|
1208
|
+
return jsonResult(await client.reviewCommunitySubmission(String(args["snapshot_id"]), { decision: "approve" }));
|
|
1209
|
+
case "reject": {
|
|
1210
|
+
if (str(args, "snapshot_id") === undefined) {
|
|
1211
|
+
return invalidArgs("reject requires `snapshot_id`");
|
|
1212
|
+
}
|
|
1213
|
+
const note = str(args, "note");
|
|
1214
|
+
if (note === undefined) {
|
|
1215
|
+
return invalidArgs("reject requires a non-empty `note`");
|
|
1216
|
+
}
|
|
1217
|
+
return jsonResult(await client.reviewCommunitySubmission(String(args["snapshot_id"]), { decision: "reject", note }));
|
|
1218
|
+
}
|
|
1219
|
+
default:
|
|
1220
|
+
return invalidArgs(`unknown community action '${action}'`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
catch (e) {
|
|
1224
|
+
return errorResult(e);
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
994
1228
|
{
|
|
995
1229
|
name: "get_skill",
|
|
996
1230
|
description: "Fetch the relay's auto-updating SKILL.md (the full Homespun usage guide) — UNAUTHENTICATED, needs no API key. Call this to self-teach the Homespun workflow (events vs records, schema grammars, the poll loop) before driving the other tools. Pass version_only:true to get just the relay's skill version string (to check if a cached copy is stale).",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homespunapps/mcp",
|
|
3
3
|
"mcpName": "io.github.aerolalit/homespun",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "Model Context Protocol (stdio) server for Homespun: lets any MCP client (Claude Desktop, Cursor, …) hand a human a rich interactive UI by URL and get structured data back.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -13,14 +13,9 @@
|
|
|
13
13
|
"relay",
|
|
14
14
|
"human-in-the-loop"
|
|
15
15
|
],
|
|
16
|
-
"homepage": "https://
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/aerolalit/homespun.git",
|
|
20
|
-
"directory": "packages/mcp"
|
|
21
|
-
},
|
|
16
|
+
"homepage": "https://homespun.dev",
|
|
22
17
|
"bugs": {
|
|
23
|
-
"url": "
|
|
18
|
+
"url": "mailto:support@homespun.dev"
|
|
24
19
|
},
|
|
25
20
|
"engines": {
|
|
26
21
|
"node": ">=20"
|
package/server.json
CHANGED
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
"title": "Homespun",
|
|
5
5
|
"description": "Hand a human a rich interactive UI by URL and get structured data back, from any MCP client.",
|
|
6
6
|
"version": "0.0.29",
|
|
7
|
-
"
|
|
8
|
-
"url": "https://github.com/aerolalit/homespun",
|
|
9
|
-
"source": "github"
|
|
10
|
-
},
|
|
7
|
+
"websiteUrl": "https://homespun.dev",
|
|
11
8
|
"packages": [
|
|
12
9
|
{
|
|
13
10
|
"registryType": "npm",
|
|
@@ -26,7 +23,7 @@
|
|
|
26
23
|
},
|
|
27
24
|
{
|
|
28
25
|
"name": "HOMESPUN_URL",
|
|
29
|
-
"description": "Homespun relay base URL. Defaults to the hosted relay https://homespun.dev; set this to point at a
|
|
26
|
+
"description": "Homespun relay base URL. Defaults to the hosted relay https://homespun.dev; set this to point at a different relay.",
|
|
30
27
|
"isRequired": false,
|
|
31
28
|
"isSecret": false
|
|
32
29
|
},
|