@homespunapps/mcp 1.6.43 → 1.6.45
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 +385 -37
- 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
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// discriminated union across a flat field set, so the handler asserts the
|
|
26
26
|
// action-specific requirements and returns a tight invalid_args error).
|
|
27
27
|
import { z } from "zod";
|
|
28
|
-
import { HomespunApiError } from "@homespunapps/core";
|
|
28
|
+
import { HomespunApiError, RELAY_FAILURE_REPORT_HINT, } from "@homespunapps/core";
|
|
29
29
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { basename } from "node:path";
|
|
31
31
|
import { resolveUrl, describeActiveConfig, clearActiveProfile, } from "./config.js";
|
|
@@ -142,6 +142,12 @@ function errorResult(e) {
|
|
|
142
142
|
payload["details"] = e.details;
|
|
143
143
|
if (e.retryable !== undefined)
|
|
144
144
|
payload["retryable"] = e.retryable;
|
|
145
|
+
// A 5xx is the relay failing, not the caller passing something wrong, so
|
|
146
|
+
// this is the one class of error worth prompting a report on. Carried as
|
|
147
|
+
// its own key rather than folded into `hint`, which the relay owns and
|
|
148
|
+
// uses to tell the agent how to fix its OWN call.
|
|
149
|
+
if (e.status >= 500)
|
|
150
|
+
payload["report"] = RELAY_FAILURE_REPORT_HINT;
|
|
145
151
|
return tagErrorCode({
|
|
146
152
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
147
153
|
isError: true,
|
|
@@ -240,20 +246,20 @@ const deployAppShape = {
|
|
|
240
246
|
app_id: z
|
|
241
247
|
.string()
|
|
242
248
|
.optional()
|
|
243
|
-
.describe("Omit to
|
|
249
|
+
.describe("Omit to create a new app; pass an existing app's id to redeploy it (a new version, compat-gated unless force:true)."),
|
|
244
250
|
html: z
|
|
245
251
|
.string()
|
|
246
252
|
.min(1)
|
|
247
253
|
.optional()
|
|
248
|
-
.describe("The app's UI as a complete HTML document (single file,
|
|
254
|
+
.describe("The app's UI as a complete HTML document (single file, with CSS and JS inline), sent inline. Capped at 2 MB of UTF-8; over that the deploy is refused with 413 document_size_exceeded. A document near the cap is almost always carrying a file inlined as a data: URI; the same file in `assets[]` is served from the app's own origin, cached separately, and does not count toward this cap. The document comes from either this field or `html_path`. Inline is the only form a hosted or remote connector with no filesystem can use, and inline `html` wins if both are given. On a redeploy an omitted `html` keeps the live document, so a manifest-only change (adding a collection, widening externalHosts) costs nothing in HTML."),
|
|
249
255
|
html_path: z
|
|
250
256
|
.string()
|
|
251
257
|
.optional()
|
|
252
|
-
.describe("
|
|
258
|
+
.describe("Absolute path to the app's HTML document, read on the MCP-server host (the machine running this connector: the relay for a hosted connector, or the CLI host for a locally-run one), not on the remote agent's machine. An alternative to inline `html` that avoids retransmitting a large HTML file on every deploy. It resolves only when the file is local to the MCP server, so it serves a locally-run connector rather than a hosted or remote one, where the path does not exist and the call returns a clean error; inline `html` is the form that works there. If both `html` and `html_path` are given, inline `html` wins."),
|
|
253
259
|
dry_run: z
|
|
254
260
|
.boolean()
|
|
255
261
|
.optional()
|
|
256
|
-
.describe("Validate only: run the full manifest + asset-shape validation, the compat gate (for a redeploy), and the schedule-timezone advisory, then return { ok, warnings, compat?, breaks? }
|
|
262
|
+
.describe("Validate only: run the full manifest + asset-shape validation, the compat gate (for a redeploy), and the schedule-timezone advisory, then return { ok, warnings, compat?, breaks? } without creating a version or mutating anything. An invalid manifest returns the same error a real deploy would; a redeploy the compat gate would refuse reports the break instead of applying it. `check` is an accepted alias."),
|
|
257
263
|
check: z.boolean().optional().describe("Alias for `dry_run`."),
|
|
258
264
|
// Optional at the SCHEMA level because a redeploy inherits an omitted
|
|
259
265
|
// manifest (#1272); the handler still refuses a create without one.
|
|
@@ -268,19 +274,19 @@ const deployAppShape = {
|
|
|
268
274
|
// exactly `type: "object"` AND turns a null into an omission.
|
|
269
275
|
manifest: z
|
|
270
276
|
.preprocess((v) => (v === null ? undefined : v), jsonObjectSchema.optional())
|
|
271
|
-
.describe("The x-homespun-manifest capability document (a JSON object).
|
|
277
|
+
.describe("The x-homespun-manifest capability document (a JSON object). Required to create; on a redeploy an omitted `manifest` keeps the live one, which fits most redeploys (the manifest was byte-identical to the previous version in 71% of real redeploys). Eight extension keys: app metadata; collections (+ per-collection write/update/read/delete role lists, where write gates creates and also updates unless the optional update list is declared); 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). The full grammar is documented in the Homespun guide that get_skill returns."),
|
|
272
278
|
visibility: z
|
|
273
279
|
.enum(["private", "link", "public"])
|
|
274
280
|
.optional()
|
|
275
|
-
.describe("
|
|
281
|
+
.describe("Create only. Default 'private' (owner plus invited members, sign-in gated). 'link' shares with anyone holding the returned share_url, whose #k= fragment carries a secret key that can be reset (rotate it via the apps tool, action share_link_rotate) to cut off everyone with the old link; a 'link' app always gets a server-generated unguessable slug. 'private' and 'public' accept an owner-chosen `slug`."),
|
|
276
282
|
slug: z
|
|
277
283
|
.string()
|
|
278
284
|
.optional()
|
|
279
|
-
.describe("
|
|
285
|
+
.describe("Create only. Accepted with visibility private or public, including the private default; rejected with explicit visibility 'link', where the slug is always server-generated."),
|
|
280
286
|
force: z
|
|
281
287
|
.boolean()
|
|
282
288
|
.optional()
|
|
283
|
-
.describe("
|
|
289
|
+
.describe("Redeploy only. Bypasses the compat gate, whether it fired on a stranded-rows narrowing or on a widening of what the install screen discloses (a removed collection is detached, never deleted)."),
|
|
284
290
|
assets: z
|
|
285
291
|
.array(z.union([
|
|
286
292
|
z.object({
|
|
@@ -305,7 +311,7 @@ const deployAppShape = {
|
|
|
305
311
|
}),
|
|
306
312
|
]))
|
|
307
313
|
.optional()
|
|
308
|
-
.describe('Optional bundle of files shipped
|
|
314
|
+
.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`; the reference form suits real images and media, where the file is uploaded once via `attachments fetch` or presign and then bound 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. On a redeploy, sent assets replace the previous version\'s set, an omitted `assets` keeps the live set (no re-upload, no re-encoding), and `assets: []` is the explicit way to clear it. Bounded by the relay\'s per-deploy asset-count cap; total bytes by the per-app blob quota.'),
|
|
309
315
|
};
|
|
310
316
|
const listRowsShape = {
|
|
311
317
|
app_id: z.string().min(1).describe("The app id."),
|
|
@@ -316,7 +322,7 @@ const listRowsShape = {
|
|
|
316
322
|
since: z
|
|
317
323
|
.string()
|
|
318
324
|
.optional()
|
|
319
|
-
.describe("Opaque cursor from a previous call's next_cursor. Also the
|
|
325
|
+
.describe("Opaque cursor from a previous call's next_cursor. Also the poll handle: pass it back to fetch only newer/changed rows."),
|
|
320
326
|
limit: z
|
|
321
327
|
.number()
|
|
322
328
|
.int()
|
|
@@ -336,7 +342,7 @@ const upsertRowShape = {
|
|
|
336
342
|
key: z
|
|
337
343
|
.string()
|
|
338
344
|
.optional()
|
|
339
|
-
.describe("Optional stable key. Reusing an existing key returns the existing row (deduped:true), or row_not_found when the collection's read list does not reach that row for
|
|
345
|
+
.describe("Optional stable key. Reusing an existing key returns the existing row (deduped:true), or row_not_found when the collection's read list does not reach that row for the caller."),
|
|
340
346
|
data: jsonValueSchema.describe("The row body - any JSON value valid against the collection's row schema (an object, or any JSON value for a schemaless collection)."),
|
|
341
347
|
};
|
|
342
348
|
const updateRowShape = {
|
|
@@ -413,7 +419,7 @@ const appsShape = {
|
|
|
413
419
|
"domain_status",
|
|
414
420
|
"domain_remove",
|
|
415
421
|
])
|
|
416
|
-
.describe("list:
|
|
422
|
+
.describe("list: the caller's owning human's apps. show/update/delete/wake: act on one app (app_id). share_link_rotate: rotate a 'link' app's share token, returning a new share_url (the old link stops working); also generates one if the app has none yet. domain_set/domain_status/domain_remove: manage the app's custom domains (app_id; domain_set also needs domain)."),
|
|
417
423
|
app_id: z
|
|
418
424
|
.string()
|
|
419
425
|
.optional()
|
|
@@ -445,12 +451,12 @@ const appsShape = {
|
|
|
445
451
|
domain: z
|
|
446
452
|
.string()
|
|
447
453
|
.optional()
|
|
448
|
-
.describe("domain_set: 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. domain_remove:
|
|
454
|
+
.describe("domain_set: 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. domain_remove: optional, the one domain to unbind - omit it to unbind them all."),
|
|
449
455
|
};
|
|
450
456
|
const membersShape = {
|
|
451
457
|
action: z
|
|
452
458
|
.enum(["add", "list", "set_role", "remove", "roles"])
|
|
453
|
-
.describe("add: invite-or-attach a member by email (app_id+email; optional custom_roles). list: the app's owner + members (app_id). set_role: replace an existing member's declared roles in place without signing them out (app_id+human_id+custom_roles, an empty list to clear). remove: drop a member (app_id+human_id). roles: the app's declared roles with what each one includes and, per collection, the
|
|
459
|
+
.describe("add: invite-or-attach a member by email (app_id+email; optional custom_roles). list: the app's owner + members (app_id). set_role: replace an existing member's declared roles in place without signing them out (app_id+human_id+custom_roles, an empty list to clear). remove: drop a member (app_id+human_id). roles: the app's declared roles with what each one includes and, per collection, the effective access a holder has (separately for members and grant-link holders, whose role floors differ) plus how many members and live grant links hold each role (app_id)."),
|
|
454
460
|
app_id: z.string().min(1).describe("The app id."),
|
|
455
461
|
email: z
|
|
456
462
|
.string()
|
|
@@ -463,7 +469,7 @@ const membersShape = {
|
|
|
463
469
|
custom_roles: z
|
|
464
470
|
.array(z.string())
|
|
465
471
|
.optional()
|
|
466
|
-
.describe("add (optional) and set_role (required). The
|
|
472
|
+
.describe("add (optional) and set_role (required). The declared roles (x-homespun-manifest.roles keys) attached to the member alongside their base member powers. A member may hold several and holds the union of what each grants, plus everything those roles `includes`. A built-in/reserved role or an undeclared role is rejected. Omit on add for an ordinary member; pass [] on set_role to clear the roles back to a plain member."),
|
|
467
473
|
human_id: z
|
|
468
474
|
.string()
|
|
469
475
|
.optional()
|
|
@@ -472,7 +478,7 @@ const membersShape = {
|
|
|
472
478
|
const ingestShape = {
|
|
473
479
|
action: z
|
|
474
480
|
.enum(["list", "rotate", "set_signing_secret", "clear_signing_secret"])
|
|
475
|
-
.describe("list: the app's inbound catch-hooks, each with its full secret URL, current rule (collection/mode/wake/handshake), and per-status delivery counts (app_id). rotate: mint a fresh URL secret for one hook and return its new URL once, invalidating the old URL immediately (app_id+name). set_signing_secret: provision or rotate a hook's
|
|
481
|
+
.describe("list: the app's inbound catch-hooks, each with its full secret URL, current rule (collection/mode/wake/handshake), and per-status delivery counts (app_id). rotate: mint a fresh URL secret for one hook and return its new URL once, invalidating the old URL immediately (app_id+name). set_signing_secret: provision or rotate a hook's opt-in signing secret, distinct from the URL secret (it is what a provider HMACs the body with); omit `secret` to mint one (returned once) or pass `secret` to store a provider-generated value verbatim (never echoed) (app_id+name). clear_signing_secret: remove a hook's signing secret (app_id+name)."),
|
|
476
482
|
app_id: z.string().min(1).describe("The app id."),
|
|
477
483
|
name: z
|
|
478
484
|
.string()
|
|
@@ -481,7 +487,7 @@ const ingestShape = {
|
|
|
481
487
|
secret: z
|
|
482
488
|
.string()
|
|
483
489
|
.optional()
|
|
484
|
-
.describe("set_signing_secret only. A provider-generated signing secret to store verbatim (the Stripe path). Omit to have the relay mint one (the GitHub path), returned
|
|
490
|
+
.describe("set_signing_secret only. A provider-generated signing secret to store verbatim (the Stripe path). Omit to have the relay mint one (the GitHub path), returned once in the response."),
|
|
485
491
|
grace_seconds: z
|
|
486
492
|
.number()
|
|
487
493
|
.optional()
|
|
@@ -498,7 +504,7 @@ const grantsShape = {
|
|
|
498
504
|
role: z
|
|
499
505
|
.string()
|
|
500
506
|
.optional()
|
|
501
|
-
.describe("mint only. A
|
|
507
|
+
.describe("mint only. A declared custom role for the app (an x-homespun-manifest.roles key). A built-in role (owner/member/agent/anyone) is rejected: a grant can never escalate."),
|
|
502
508
|
mode: z
|
|
503
509
|
.enum(["once", "multi"])
|
|
504
510
|
.optional()
|
|
@@ -522,16 +528,132 @@ const grantsShape = {
|
|
|
522
528
|
pin_row_key: z
|
|
523
529
|
.string()
|
|
524
530
|
.optional()
|
|
525
|
-
.describe("mint only. Optional narrowing pin to a single row key.
|
|
531
|
+
.describe("mint only. Optional narrowing pin to a single row key. Narrows within the role (never widens). Mutually exclusive with pin_where."),
|
|
526
532
|
pin_where: z
|
|
527
533
|
.array(z.unknown())
|
|
528
534
|
.optional()
|
|
529
|
-
.describe("mint only. Optional narrowing pin as Wave C2 where conditions ({field, op, value}[]).
|
|
535
|
+
.describe("mint only. Optional narrowing pin as Wave C2 where conditions ({field, op, value}[]). Narrows within the role (never widens). Mutually exclusive with pin_row_key."),
|
|
530
536
|
grant_id: z
|
|
531
537
|
.string()
|
|
532
538
|
.optional()
|
|
533
539
|
.describe("revoke only. The grant link id (see list's `id` field)."),
|
|
534
540
|
};
|
|
541
|
+
const credentialsShape = {
|
|
542
|
+
action: z
|
|
543
|
+
.enum(["mint", "list", "pause", "resume", "rotate", "revoke"])
|
|
544
|
+
.describe("mint: create a scoped service credential, the bearer token an app owner points a backend they host themselves at (app_id; optional mode/grants/members/label/ttl_seconds). list: the app's credentials, their allowlist and status, never a token (app_id). pause: reversibly stop one, in force on its very next request (app_id+credential_id). resume: undo a pause; never undoes a revoke, which is permanent (app_id+credential_id). rotate: issue a fresh token and keep the old one working for an overlap window so a running backend picks it up without a gap (app_id+credential_id; optional overlap_seconds). revoke: kill one permanently and idempotently (app_id+credential_id)."),
|
|
545
|
+
app_id: z.string().min(1).describe("The app id."),
|
|
546
|
+
mode: z
|
|
547
|
+
.enum(["explicit", "following"])
|
|
548
|
+
.optional()
|
|
549
|
+
.describe("mint only. Defaults to explicit: an unnamed collection is denied, so the credential can never reach anything it was not handed (the shape for a contractor's backend). following: an unnamed collection falls through to the owner's own authority, so the credential tracks the app as it grows and each `grants` entry only narrows one collection (the shape for the owner's own backend). Neither mode can ever exceed what the app's owner could do; the effective permission is always the intersection."),
|
|
550
|
+
grants: z
|
|
551
|
+
.array(z.object({
|
|
552
|
+
collection: z.string().min(1),
|
|
553
|
+
ops: z.array(z.enum(["read", "create", "update", "delete"])),
|
|
554
|
+
scope: z
|
|
555
|
+
.literal("own")
|
|
556
|
+
.optional()
|
|
557
|
+
.describe("Narrows every row-addressed op to rows this credential itself wrote last. Inert for create."),
|
|
558
|
+
}))
|
|
559
|
+
.optional()
|
|
560
|
+
.describe("mint only. The allowlist: one entry per collection naming which of read/create/update/delete this credential may attempt there (an entry may name zero ops, which under `following` is how one collection is carved out of an otherwise app-wide credential). A collection named here must be a real declared collection on the app; a typo is rejected with a 400 rather than silently doing nothing."),
|
|
561
|
+
members: z
|
|
562
|
+
.boolean()
|
|
563
|
+
.optional()
|
|
564
|
+
.describe("mint only. Opt in to the app's member directory appearing in this credential's boot/hello payloads. Defaults to false: a credential that never learns a member id cannot stamp one into a relation field."),
|
|
565
|
+
label: z
|
|
566
|
+
.string()
|
|
567
|
+
.optional()
|
|
568
|
+
.describe("mint only. Optional owner-facing label shown in the credential list."),
|
|
569
|
+
ttl_seconds: z
|
|
570
|
+
.number()
|
|
571
|
+
.int()
|
|
572
|
+
.positive()
|
|
573
|
+
.nullable()
|
|
574
|
+
.optional()
|
|
575
|
+
.describe("mint only. Omit for the server's bounded default (365 days, clamped to a server maximum). null means no expiry, the explicit opt-in a long-running backend asks for; it is never the default."),
|
|
576
|
+
credential_id: z
|
|
577
|
+
.string()
|
|
578
|
+
.optional()
|
|
579
|
+
.describe("pause / resume / rotate / revoke. The credential id (see list's `id` field)."),
|
|
580
|
+
overlap_seconds: z
|
|
581
|
+
.number()
|
|
582
|
+
.int()
|
|
583
|
+
.min(0)
|
|
584
|
+
.optional()
|
|
585
|
+
.describe('rotate only. How long the superseded token keeps resolving, so a running backend can pick up the new one with no gap. Defaults to the server default (1 day); 0 kills the old token immediately, the "this leaked" case.'),
|
|
586
|
+
};
|
|
587
|
+
const connectionsShape = {
|
|
588
|
+
action: z
|
|
589
|
+
.enum(["create", "list", "delete", "consent_url"])
|
|
590
|
+
.describe("create: store a webhook connection, a stored credential (static header token or a full generic OAuth2 client) a manifest webhook rule authenticates its target with (app_id+name+allowed_host, plus kind-specific fields). list: the app's connections as metadata plus a non-reversible fingerprint, never any secret (app_id). delete: idempotent (app_id+name). consent_url: build (never fetch) the browser URL that completes an oauth2 connection's owner consent (app_id+name); hand it to the signed-in owner to open, since an agent key cannot complete OAuth consent itself."),
|
|
591
|
+
app_id: z.string().min(1).describe("The app id."),
|
|
592
|
+
name: z
|
|
593
|
+
.string()
|
|
594
|
+
.optional()
|
|
595
|
+
.describe("create / delete / consent_url. The connection name (lowercase, starting alphanumeric, up to 64 chars) that a manifest webhook rule's `connection` field references."),
|
|
596
|
+
kind: z
|
|
597
|
+
.enum(["static", "oauth2"])
|
|
598
|
+
.optional()
|
|
599
|
+
.describe("create only. Defaults to `static`."),
|
|
600
|
+
provider: z
|
|
601
|
+
.string()
|
|
602
|
+
.optional()
|
|
603
|
+
.describe('create only. Freeform display label only, e.g. "hubspot"; not validated against any allowlist.'),
|
|
604
|
+
label: z
|
|
605
|
+
.string()
|
|
606
|
+
.optional()
|
|
607
|
+
.describe("create only. Optional owner-facing label."),
|
|
608
|
+
allowed_host: z
|
|
609
|
+
.string()
|
|
610
|
+
.optional()
|
|
611
|
+
.describe('create only, required for both kinds. The host-binding exfiltration defence: an exact DNS host ("api.hubapi.com") or a single leftmost wildcard ("*.zohoapis.com"). The stored credential is attached to a delivery only when its url host matches; a rule later repointed elsewhere fails delivery rather than sending the secret to the wrong host.'),
|
|
612
|
+
header_name: z
|
|
613
|
+
.string()
|
|
614
|
+
.optional()
|
|
615
|
+
.describe('create only (static). The header the credential rides in. Defaults to "Authorization".'),
|
|
616
|
+
header_value: z
|
|
617
|
+
.string()
|
|
618
|
+
.optional()
|
|
619
|
+
.describe('create only, required for kind=static. The header value to send, e.g. "Bearer sk_live_...". Encrypted at rest and never returned by any call.'),
|
|
620
|
+
authorize_url: z
|
|
621
|
+
.string()
|
|
622
|
+
.optional()
|
|
623
|
+
.describe("create only, required for kind=oauth2. The provider's OAuth2 authorize endpoint (https; rejected if it resolves to a private/loopback/metadata address)."),
|
|
624
|
+
token_endpoint: z
|
|
625
|
+
.string()
|
|
626
|
+
.optional()
|
|
627
|
+
.describe("create only, required for kind=oauth2. The provider's OAuth2 token endpoint (same https + SSRF rules as authorize_url)."),
|
|
628
|
+
client_id: z
|
|
629
|
+
.string()
|
|
630
|
+
.optional()
|
|
631
|
+
.describe("create only, required for kind=oauth2. Your OAuth2 app's client id."),
|
|
632
|
+
client_secret: z
|
|
633
|
+
.string()
|
|
634
|
+
.optional()
|
|
635
|
+
.describe("create only, required for kind=oauth2. Your OAuth2 app's client secret. Encrypted at rest and never returned by any call."),
|
|
636
|
+
scopes: z
|
|
637
|
+
.string()
|
|
638
|
+
.optional()
|
|
639
|
+
.describe("create only (oauth2). Space-delimited scopes for the authorize request."),
|
|
640
|
+
auth_scheme: z
|
|
641
|
+
.string()
|
|
642
|
+
.optional()
|
|
643
|
+
.describe('create only (oauth2). The scheme the access token is sent under. Defaults to "Bearer"; set e.g. "Zoho-oauthtoken" for a non-Bearer provider.'),
|
|
644
|
+
instance_field: z
|
|
645
|
+
.string()
|
|
646
|
+
.optional()
|
|
647
|
+
.describe('create only (oauth2). The name of a token-response JSON field holding the API base URL (e.g. "instance_url"). When set, the relay re-binds allowed_host to that host after consent and resolves relative rule urls against it.'),
|
|
648
|
+
auth_params: z
|
|
649
|
+
.record(z.string(), z.unknown())
|
|
650
|
+
.optional()
|
|
651
|
+
.describe("create only (oauth2). Extra key/values merged into the authorize redirect (e.g. to request offline access)."),
|
|
652
|
+
token_params: z
|
|
653
|
+
.record(z.string(), z.unknown())
|
|
654
|
+
.optional()
|
|
655
|
+
.describe("create only (oauth2). Extra key/values merged into the token POST."),
|
|
656
|
+
};
|
|
535
657
|
const attachmentsShape = {
|
|
536
658
|
action: z
|
|
537
659
|
.enum([
|
|
@@ -565,7 +687,7 @@ const attachmentsShape = {
|
|
|
565
687
|
file_path: z
|
|
566
688
|
.string()
|
|
567
689
|
.optional()
|
|
568
|
-
.describe("upload:
|
|
690
|
+
.describe("upload: absolute path to a file read on the server host running this MCP connector (the relay), not the calling agent's machine. It resolves only when the file is local to the relay (e.g. a locally-run CLI); a hosted or remote agent supplies the bytes as `content_base64` instead."),
|
|
569
691
|
source_url: z
|
|
570
692
|
.string()
|
|
571
693
|
.optional()
|
|
@@ -586,11 +708,11 @@ const attachmentsShape = {
|
|
|
586
708
|
mime: z
|
|
587
709
|
.string()
|
|
588
710
|
.optional()
|
|
589
|
-
.describe("upload/presign: advisory Content-Type. The relay
|
|
711
|
+
.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)."),
|
|
590
712
|
out_path: z
|
|
591
713
|
.string()
|
|
592
714
|
.optional()
|
|
593
|
-
.describe("download:
|
|
715
|
+
.describe("download: absolute path to write the bytes to. If omitted, the bytes are returned base64-encoded in the result."),
|
|
594
716
|
cursor: z.string().optional().describe("list pagination cursor."),
|
|
595
717
|
limit: z
|
|
596
718
|
.number()
|
|
@@ -626,13 +748,13 @@ const tasteShape = {
|
|
|
626
748
|
const keyShape = {
|
|
627
749
|
action: z
|
|
628
750
|
.enum(["list", "revoke", "mint"])
|
|
629
|
-
.describe("The calling agent's API key. list: key info (agent_id, key_prefix, timestamps). mint:
|
|
751
|
+
.describe("The calling agent's API key. list: key info (agent_id, key_prefix, timestamps). mint: mints a sibling API key for the calling agent's own identity (same scope/ownership) and returns its raw value once, which is what hands a CLI or child process a working credential; the sibling is a distinct key that shows up in a subsequent `list` made with it, the owner can revoke it, and the raw value is never retrievable again. mint always acts on the calling agent, never another agent's id. revoke: self-destructs the agent's own key, which stops working immediately and is irreversible (requires confirm:true)."),
|
|
630
752
|
confirm: z.boolean().optional().describe("Required (true) for revoke."),
|
|
631
753
|
};
|
|
632
754
|
const feedbackShape = {
|
|
633
755
|
action: z
|
|
634
756
|
.enum(["create", "list"])
|
|
635
|
-
.describe("
|
|
757
|
+
.describe("Reports a problem with homespun itself to the relay operator. create: files one bug|feature|note with a message and an optional app_id. list: this agent's own submissions, newest first, which is what distinguishes a new failure from one already reported."),
|
|
636
758
|
type: z
|
|
637
759
|
.enum(["bug", "feature", "note"])
|
|
638
760
|
.optional()
|
|
@@ -660,7 +782,7 @@ const feedbackShape = {
|
|
|
660
782
|
const agentShape = {
|
|
661
783
|
action: z
|
|
662
784
|
.enum(["whoami", "claim", "logout"])
|
|
663
|
-
.describe("Agent identity. whoami: show the resolved relay URL, active profile, and whether a key is configured (no network, no secrets). claim: bind this agent to a human via a one-shot claim code the human generated in their Settings UI (one-way). logout:
|
|
785
|
+
.describe("Agent identity. whoami: show the resolved relay URL, active profile, and whether a key is configured (no network, no secrets). claim: bind this agent to a human via a one-shot claim code the human generated in their Settings UI (one-way). logout: clears the locally-saved key/profile; the key is not revoked on the relay, which is what the key tool's revoke action does."),
|
|
664
786
|
code: z
|
|
665
787
|
.string()
|
|
666
788
|
.optional()
|
|
@@ -679,7 +801,7 @@ const communityShape = {
|
|
|
679
801
|
"reject",
|
|
680
802
|
"set_trust_level",
|
|
681
803
|
])
|
|
682
|
-
.describe("publish:
|
|
804
|
+
.describe("publish: publishes one of the caller's apps as a community template (app_id; optional title/description/category/tags). Privacy consequence: 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, so an app whose seedOnInstall collections hold real personal data (names, emails, addresses, messages, anything private) is not safe to publish: seed data must be example-only. attest_example_only:true records that this was checked. 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, and the response's expedited/auto_approved fields report which path it took. unpublish: takes one of the caller's 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 belongs to someone else reads as not found either way. Publishing a new version is what puts 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-uploaded with the attachments tool (scope agent) and passed as its attachment id. A template installed with connect_steps provisions hook URLs, which the `ingest` tool's list action returns for the new app_id, ready to wire into the external service. install: installs a template by `ref` for the caller, whose owning human becomes the owner. `config` is { stepKey: value } from the contract: a 'config' step's value is a string, an 'upload' step's value is a pre-uploaded attachment id. An omitted required step 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')."),
|
|
683
805
|
ref: z
|
|
684
806
|
.string()
|
|
685
807
|
.optional()
|
|
@@ -691,7 +813,7 @@ const communityShape = {
|
|
|
691
813
|
app_id: z
|
|
692
814
|
.string()
|
|
693
815
|
.optional()
|
|
694
|
-
.describe("publish only. The id of an app
|
|
816
|
+
.describe("publish only. The id of an app the caller owns, to publish."),
|
|
695
817
|
title: z
|
|
696
818
|
.string()
|
|
697
819
|
.optional()
|
|
@@ -715,7 +837,7 @@ const communityShape = {
|
|
|
715
837
|
slug: z
|
|
716
838
|
.string()
|
|
717
839
|
.optional()
|
|
718
|
-
.describe("publish only. Optional per-publisher slug (lowercase, 3 to 48 chars, hyphens). Gives the template a namespaced id <
|
|
840
|
+
.describe("publish only. Optional per-publisher slug (lowercase, 3 to 48 chars, hyphens). Gives the template a namespaced id <handle>/<slug>; a republish reuses the slug and must bump the version. If omitted, a slug is derived from the title instead of leaving the template unnamed, so this field matters only when a specific url is wanted. Slugs are immutable: renaming the template later does not move its url, so the slug chosen here is permanent."),
|
|
719
841
|
version: z
|
|
720
842
|
.string()
|
|
721
843
|
.optional()
|
|
@@ -772,7 +894,7 @@ const communityShape = {
|
|
|
772
894
|
attest_example_only: z
|
|
773
895
|
.boolean()
|
|
774
896
|
.optional()
|
|
775
|
-
.describe("publish only.
|
|
897
|
+
.describe("publish only. True attests that the template content and the captured seed rows contain no real personal data. Publishing makes both public to every platform user, so seed data (the live rows of the app's seedOnInstall collections) must be example-only, never real names/emails/addresses/private messages. Recorded and shown to the reviewer; omitting it still publishes but is flagged to the operator as not attested."),
|
|
776
898
|
snapshot_id: z
|
|
777
899
|
.string()
|
|
778
900
|
.optional()
|
|
@@ -804,7 +926,7 @@ const communityShape = {
|
|
|
804
926
|
const publisherShape = {
|
|
805
927
|
action: z
|
|
806
928
|
.enum(["claim", "get", "update"])
|
|
807
|
-
.describe("get:
|
|
929
|
+
.describe("get: returns the caller's publisher profile (handle, tenure, counters). claim: sets the caller's @-handle, once (handle arg; lowercase, 3 to 32 chars, permanent after claiming; needs a verified email). update: changes the caller's public display_name/bio/url (any of them; needs a verified email)."),
|
|
808
930
|
handle: z
|
|
809
931
|
.string()
|
|
810
932
|
.optional()
|
|
@@ -828,7 +950,7 @@ const publisherShape = {
|
|
|
828
950
|
const reviewShape = {
|
|
829
951
|
action: z
|
|
830
952
|
.enum(["create", "respond", "report", "remove", "unhold"])
|
|
831
|
-
.describe(
|
|
953
|
+
.describe("create: leaves a star rating (1..5) and optional body on a community template the caller has installed (identified by `template` \"<handle>/<slug>\" or by `handle`+`slug`); requires a verified email, and one review per install. A body containing a link or contact email is auto-held for a moderator before it shows. respond: replies to a review of one of the caller's own templates (review_id + response; null clears it). report: flags a review for the relay's moderators (review_id + reason; one report per account). remove / unhold are relay-operator-only moderation actions on a review_id: remove takes a review down (adjusting the aggregate), unhold publishes a previously auto-held review."),
|
|
832
954
|
template: z
|
|
833
955
|
.string()
|
|
834
956
|
.optional()
|
|
@@ -879,7 +1001,7 @@ export const TOOLS = [
|
|
|
879
1001
|
// ----- v2 app lifecycle + data (discrete, hot-path) -----------------------
|
|
880
1002
|
{
|
|
881
1003
|
name: "deploy_app",
|
|
882
|
-
description: "Deploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL.\n\
|
|
1004
|
+
description: "Deploy a v2 app: an HTML document plus a capability manifest, hosted at its own URL.\n\nA redeploy only needs the content that changed. Every content field is optional when `app_id` is given, and an omitted one keeps what is live: omit `manifest` for an HTML-only change, omit `html` for a manifest-only change, omit `assets` to keep the current files. This is the cheap path and the default, because an omitted field costs no output tokens at all: a one-line colour change does not resend the whole document, and a manifest edit does not resend it either. A field only needs sending when its content differs from what is live. `assets: []` is the explicit way to clear the asset set, and omitting all three is refused, since there would be nothing to change.\n\nThe manifest carries eight extension keys: app metadata; collections, with per-collection write, update, read and delete role lists, where write gates creates and also gates updates unless an update list is declared; externalHosts, a fetch allowlist; cdn, to allow CDN scripts and styles; capabilities, for Permissions-Policy opt-ins; embeds, an iframe frame-src allowlist; notify, for email-on-row rules; and webhooks, for signed HTTP POST on-row rules. The manifest grammar is documented in the Homespun guide that get_skill returns.\n\nPass no `app_id` to create, which mints a slug and URL and requires both `html` and `manifest`, or pass `app_id` to redeploy an existing app. Supply the HTML inline as `html`, or as `html_path`, an absolute path read on the MCP-server host, which is the relay for a hosted connector or the CLI host for a locally-run one, and not the remote agent's machine; it avoids retransmitting a large HTML file on every deploy, only a locally-run connector can read it, and inline `html` wins if both are given. `dry_run:true` (alias `check`) validates only: it runs the full manifest and asset validation, the redeploy compat gate and the schedule-timezone advisory, then returns { ok, warnings, compat?, breaks? } without creating a version or mutating anything, and it resolves omitted fields the same way a real deploy would, so it reports on exactly the deploy that would run.\n\nA redeploy is refused with manifest_incompatible_redeploy, unless force:true, when it would strand rows already written (dropping a collection, tightening a schema, flipping appendOnly), or when it would widen what the app's install screen discloses: a collection's read reaching further than the live manifest, a capability added, cdn turned on, or a host added to externalHosts, embeds or a webhook target. The break quotes the sentence a user would now be asked to approve. Taking access away never prompts: dropping a role, dropping a capability, host or webhook, turning cdn off, or adding update:[\\\"creator\\\"] to a write:[\\\"anyone\\\"] collection, all redeploy clean. A removed collection is detached rather than deleted.\n\nImages, fonts, audio, video and data files ship with the app in the same call via `assets[]`. Each is validated and 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 such as `<img src=\\\"frames/000.jpg\\\">`; media and font paths support HTTP Range for seeking. A redeploy's assets replace the previous version's set when sent, carry over when omitted, and are cleared by `assets: []`.\n\nReturns { app_id, slug, url, version, visibility, created } on create, or { app_id, version, compat, breaks? } on redeploy.",
|
|
883
1005
|
inputSchema: deployAppShape,
|
|
884
1006
|
annotations: {
|
|
885
1007
|
title: "Deploy App",
|
|
@@ -1080,7 +1202,7 @@ export const TOOLS = [
|
|
|
1080
1202
|
},
|
|
1081
1203
|
{
|
|
1082
1204
|
name: "delete_row",
|
|
1083
|
-
description: "Soft-delete a row from a v2 app's collection.
|
|
1205
|
+
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 }.",
|
|
1084
1206
|
inputSchema: deleteRowShape,
|
|
1085
1207
|
annotations: {
|
|
1086
1208
|
title: "Delete Row",
|
|
@@ -1104,7 +1226,7 @@ export const TOOLS = [
|
|
|
1104
1226
|
},
|
|
1105
1227
|
{
|
|
1106
1228
|
name: "list_deleted_rows",
|
|
1107
|
-
description: "List a collection's recently deleted rows: the recovery bin. Deleting a row is a
|
|
1229
|
+
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
1230
|
inputSchema: listDeletedRowsShape,
|
|
1109
1231
|
annotations: {
|
|
1110
1232
|
title: "List Deleted Rows",
|
|
@@ -1427,6 +1549,232 @@ export const TOOLS = [
|
|
|
1427
1549
|
},
|
|
1428
1550
|
},
|
|
1429
1551
|
// ----- consolidated management tools --------------------------------------
|
|
1552
|
+
{
|
|
1553
|
+
name: "credentials",
|
|
1554
|
+
description: "A v2 app's scoped service credentials (#1354, #1355): the bearer token an app owner points a backend they host themselves at, so their own server can read and write the app's data without holding the owner's full authority. Effective permission is always the intersection of the allowlist and what the app's owner could do, so a credential can only ever narrow, never widen, and it carries no role. Actions: mint creates one and returns its raw `token` shown once, never recoverable afterward (only its hash is stored); list returns the app's credentials with their allowlist and status, never any token material; pause reversibly stops one; resume undoes a pause (never a revoke, which is permanent); rotate issues a fresh token while the old one keeps working for an overlap window, so a running backend picks up the new token with no outage; revoke kills one permanently. Every action here is owner-or-owning-agent only: a service credential itself can reach none of these, by construction, so it can never mint or widen a sibling of itself.",
|
|
1555
|
+
inputSchema: credentialsShape,
|
|
1556
|
+
// Consolidated tool: read action (list) + mutating ones (mint/pause/
|
|
1557
|
+
// resume/rotate/revoke). Hint reflects revoke/rotate, the most-privileged
|
|
1558
|
+
// actions, matching the grants/ingest convention.
|
|
1559
|
+
annotations: {
|
|
1560
|
+
title: "Manage App Service Credentials",
|
|
1561
|
+
readOnlyHint: false,
|
|
1562
|
+
// Destructive: `revoke` permanently kills a live credential and
|
|
1563
|
+
// `rotate` invalidates the superseded token once its overlap window
|
|
1564
|
+
// lapses (immediately when overlap_seconds is 0).
|
|
1565
|
+
destructiveHint: true,
|
|
1566
|
+
// NOT idempotent: `mint` and `rotate` each produce a fresh secret, so a
|
|
1567
|
+
// retried call leaves a second live credential behind rather than having
|
|
1568
|
+
// no additional effect. Matches the `key` tool, which is the same shape.
|
|
1569
|
+
idempotentHint: false,
|
|
1570
|
+
openWorldHint: false,
|
|
1571
|
+
},
|
|
1572
|
+
handler: async (client, args) => {
|
|
1573
|
+
const action = String(args["action"]);
|
|
1574
|
+
if (str(args, "app_id") === undefined) {
|
|
1575
|
+
return invalidArgs(`${action} requires \`app_id\``);
|
|
1576
|
+
}
|
|
1577
|
+
const appId = String(args["app_id"]);
|
|
1578
|
+
try {
|
|
1579
|
+
switch (action) {
|
|
1580
|
+
case "mint": {
|
|
1581
|
+
const grants = Array.isArray(args["grants"])
|
|
1582
|
+
? args["grants"]
|
|
1583
|
+
: undefined;
|
|
1584
|
+
return jsonResult(await client.mintAppCredential(appId, {
|
|
1585
|
+
...(args["mode"] !== undefined
|
|
1586
|
+
? { mode: args["mode"] }
|
|
1587
|
+
: {}),
|
|
1588
|
+
...(grants !== undefined ? { grants } : {}),
|
|
1589
|
+
...(typeof args["members"] === "boolean"
|
|
1590
|
+
? { members: args["members"] }
|
|
1591
|
+
: {}),
|
|
1592
|
+
...(str(args, "label") !== undefined
|
|
1593
|
+
? { label: String(args["label"]) }
|
|
1594
|
+
: {}),
|
|
1595
|
+
...(args["ttl_seconds"] !== undefined
|
|
1596
|
+
? { ttlSeconds: args["ttl_seconds"] }
|
|
1597
|
+
: {}),
|
|
1598
|
+
}));
|
|
1599
|
+
}
|
|
1600
|
+
case "list":
|
|
1601
|
+
return jsonResult(await client.listAppCredentials(appId));
|
|
1602
|
+
case "pause": {
|
|
1603
|
+
if (str(args, "credential_id") === undefined) {
|
|
1604
|
+
return invalidArgs("pause requires `credential_id`");
|
|
1605
|
+
}
|
|
1606
|
+
await client.pauseAppCredential(appId, String(args["credential_id"]));
|
|
1607
|
+
return jsonResult({
|
|
1608
|
+
app_id: appId,
|
|
1609
|
+
credential_id: args["credential_id"],
|
|
1610
|
+
paused: true,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
case "resume": {
|
|
1614
|
+
if (str(args, "credential_id") === undefined) {
|
|
1615
|
+
return invalidArgs("resume requires `credential_id`");
|
|
1616
|
+
}
|
|
1617
|
+
await client.resumeAppCredential(appId, String(args["credential_id"]));
|
|
1618
|
+
return jsonResult({
|
|
1619
|
+
app_id: appId,
|
|
1620
|
+
credential_id: args["credential_id"],
|
|
1621
|
+
resumed: true,
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
case "rotate": {
|
|
1625
|
+
if (str(args, "credential_id") === undefined) {
|
|
1626
|
+
return invalidArgs("rotate requires `credential_id`");
|
|
1627
|
+
}
|
|
1628
|
+
return jsonResult(await client.rotateAppCredential(appId, String(args["credential_id"]), {
|
|
1629
|
+
...(typeof args["overlap_seconds"] === "number"
|
|
1630
|
+
? { overlapSeconds: args["overlap_seconds"] }
|
|
1631
|
+
: {}),
|
|
1632
|
+
}));
|
|
1633
|
+
}
|
|
1634
|
+
case "revoke": {
|
|
1635
|
+
if (str(args, "credential_id") === undefined) {
|
|
1636
|
+
return invalidArgs("revoke requires `credential_id`");
|
|
1637
|
+
}
|
|
1638
|
+
await client.revokeAppCredential(appId, String(args["credential_id"]));
|
|
1639
|
+
return jsonResult({
|
|
1640
|
+
app_id: appId,
|
|
1641
|
+
credential_id: args["credential_id"],
|
|
1642
|
+
revoked: true,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
default:
|
|
1646
|
+
return invalidArgs(`unknown credentials action '${action}'`);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
catch (e) {
|
|
1650
|
+
return errorResult(e);
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: "connections",
|
|
1656
|
+
description: "A v2 app's Connections: the stored credential (a static header token, or a full generic OAuth2 client) a manifest webhook rule authenticates its delivery target with, bound to a host so the credential can never be exfiltrated to another one. There is no update action: change a connection by deleting and recreating it. Actions: create stores a static or oauth2 connection and returns its metadata, never the secret; list returns the app's connections as metadata plus a non-reversible fingerprint, never any secret; delete is idempotent; consent_url builds (never fetches) the browser URL that completes an oauth2 connection's consent, since that is inherently a human-in-a-browser step an agent key cannot complete. A newly created oauth2 connection starts in `pending_auth` until the owner opens the consent_url and approves.",
|
|
1657
|
+
inputSchema: connectionsShape,
|
|
1658
|
+
// Consolidated tool: read action (list) + mutating ones (create/delete).
|
|
1659
|
+
// Hint reflects delete, the most-privileged action.
|
|
1660
|
+
annotations: {
|
|
1661
|
+
title: "Manage App Connections",
|
|
1662
|
+
readOnlyHint: false,
|
|
1663
|
+
// Destructive: `delete` stops any webhook still using that connection
|
|
1664
|
+
// from authenticating.
|
|
1665
|
+
destructiveHint: true,
|
|
1666
|
+
// NOT idempotent: `create` adds another connection each time it runs, so
|
|
1667
|
+
// a retried call leaves a second one behind rather than having no
|
|
1668
|
+
// additional effect. Matches the `key` tool, which is the same shape.
|
|
1669
|
+
idempotentHint: false,
|
|
1670
|
+
openWorldHint: false,
|
|
1671
|
+
},
|
|
1672
|
+
handler: async (client, args) => {
|
|
1673
|
+
const action = String(args["action"]);
|
|
1674
|
+
if (str(args, "app_id") === undefined) {
|
|
1675
|
+
return invalidArgs(`${action} requires \`app_id\``);
|
|
1676
|
+
}
|
|
1677
|
+
const appId = String(args["app_id"]);
|
|
1678
|
+
try {
|
|
1679
|
+
switch (action) {
|
|
1680
|
+
case "create": {
|
|
1681
|
+
if (str(args, "name") === undefined) {
|
|
1682
|
+
return invalidArgs("create requires `name`");
|
|
1683
|
+
}
|
|
1684
|
+
if (str(args, "allowed_host") === undefined) {
|
|
1685
|
+
return invalidArgs("create requires `allowed_host`");
|
|
1686
|
+
}
|
|
1687
|
+
const kind = args["kind"] ?? "static";
|
|
1688
|
+
if (kind === "oauth2") {
|
|
1689
|
+
if (str(args, "authorize_url") === undefined ||
|
|
1690
|
+
str(args, "token_endpoint") === undefined ||
|
|
1691
|
+
str(args, "client_id") === undefined ||
|
|
1692
|
+
str(args, "client_secret") === undefined) {
|
|
1693
|
+
return invalidArgs("create (kind=oauth2) requires `authorize_url`, `token_endpoint`, `client_id` and `client_secret`");
|
|
1694
|
+
}
|
|
1695
|
+
return jsonResult(await client.createConnection(appId, {
|
|
1696
|
+
name: String(args["name"]),
|
|
1697
|
+
kind: "oauth2",
|
|
1698
|
+
allowedHost: String(args["allowed_host"]),
|
|
1699
|
+
authorizeUrl: String(args["authorize_url"]),
|
|
1700
|
+
tokenEndpoint: String(args["token_endpoint"]),
|
|
1701
|
+
clientId: String(args["client_id"]),
|
|
1702
|
+
clientSecret: String(args["client_secret"]),
|
|
1703
|
+
...(str(args, "provider") !== undefined
|
|
1704
|
+
? { provider: String(args["provider"]) }
|
|
1705
|
+
: {}),
|
|
1706
|
+
...(str(args, "label") !== undefined
|
|
1707
|
+
? { label: String(args["label"]) }
|
|
1708
|
+
: {}),
|
|
1709
|
+
...(str(args, "scopes") !== undefined
|
|
1710
|
+
? { scopes: String(args["scopes"]) }
|
|
1711
|
+
: {}),
|
|
1712
|
+
...(str(args, "auth_scheme") !== undefined
|
|
1713
|
+
? { authScheme: String(args["auth_scheme"]) }
|
|
1714
|
+
: {}),
|
|
1715
|
+
...(str(args, "instance_field") !== undefined
|
|
1716
|
+
? { instanceField: String(args["instance_field"]) }
|
|
1717
|
+
: {}),
|
|
1718
|
+
...(isPlainObject(args["auth_params"])
|
|
1719
|
+
? { authParams: args["auth_params"] }
|
|
1720
|
+
: {}),
|
|
1721
|
+
...(isPlainObject(args["token_params"])
|
|
1722
|
+
? { tokenParams: args["token_params"] }
|
|
1723
|
+
: {}),
|
|
1724
|
+
}));
|
|
1725
|
+
}
|
|
1726
|
+
if (str(args, "header_value") === undefined) {
|
|
1727
|
+
return invalidArgs("create requires `header_value` for a static connection");
|
|
1728
|
+
}
|
|
1729
|
+
return jsonResult(await client.createConnection(appId, {
|
|
1730
|
+
name: String(args["name"]),
|
|
1731
|
+
kind: "static",
|
|
1732
|
+
allowedHost: String(args["allowed_host"]),
|
|
1733
|
+
headerValue: String(args["header_value"]),
|
|
1734
|
+
headerName: str(args, "header_name") !== undefined
|
|
1735
|
+
? String(args["header_name"])
|
|
1736
|
+
: "Authorization",
|
|
1737
|
+
...(str(args, "provider") !== undefined
|
|
1738
|
+
? { provider: String(args["provider"]) }
|
|
1739
|
+
: {}),
|
|
1740
|
+
...(str(args, "label") !== undefined
|
|
1741
|
+
? { label: String(args["label"]) }
|
|
1742
|
+
: {}),
|
|
1743
|
+
}));
|
|
1744
|
+
}
|
|
1745
|
+
case "list":
|
|
1746
|
+
return jsonResult(await client.listConnections(appId));
|
|
1747
|
+
case "delete": {
|
|
1748
|
+
if (str(args, "name") === undefined) {
|
|
1749
|
+
return invalidArgs("delete requires `name`");
|
|
1750
|
+
}
|
|
1751
|
+
await client.deleteConnection(appId, String(args["name"]));
|
|
1752
|
+
return jsonResult({
|
|
1753
|
+
app_id: appId,
|
|
1754
|
+
name: args["name"],
|
|
1755
|
+
deleted: true,
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
case "consent_url": {
|
|
1759
|
+
if (str(args, "name") === undefined) {
|
|
1760
|
+
return invalidArgs("consent_url requires `name`");
|
|
1761
|
+
}
|
|
1762
|
+
return jsonResult({
|
|
1763
|
+
app_id: appId,
|
|
1764
|
+
name: args["name"],
|
|
1765
|
+
authorize_url: client.connectionAuthorizeUrl(appId, String(args["name"])),
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
default:
|
|
1769
|
+
return invalidArgs(`unknown connections action '${action}'`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
catch (e) {
|
|
1773
|
+
return errorResult(e);
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
},
|
|
1777
|
+
// ----- consolidated management tools --------------------------------------
|
|
1430
1778
|
{
|
|
1431
1779
|
name: "ingest",
|
|
1432
1780
|
description: "A v2 app's inbound catch-hooks (inbound-webhooks). A catch-hook lets an external system such as Stripe, Zapier, Make, Home Assistant or an email router POST JSON to a secret URL that writes into a declared collection, so the app receives data with no agent online. Hooks are declared in the manifest (x-homespun-manifest.ingest) and materialized at deploy, so this tool has no create or delete: it reads back the URL, rotates a leaked one, and manages the opt-in signing secret. After deploying a manifest that declares a hook, list is what yields the exact URL to paste into the external system. Actions: list returns the app's hooks, each with its full secret URL, current rule collection, mode, wake and handshake settings, per-status delivery counts and signing-secret state; rotate mints a fresh URL secret for one hook by name and returns the new url once, after which the old url stops working immediately with no redeploy needed; set_signing_secret provisions or rotates a hook's signing secret, which is a different secret from the URL and is what a provider HMACs the body with, minting one returned once when `secret` is omitted or storing a provider value verbatim when it is passed, and never echoing it back; clear_signing_secret removes it. Signature verification currently ships dark: nothing verifies a signature yet.",
|
|
@@ -1761,7 +2109,7 @@ export const TOOLS = [
|
|
|
1761
2109
|
},
|
|
1762
2110
|
{
|
|
1763
2111
|
name: "feedback",
|
|
1764
|
-
description: "
|
|
2112
|
+
description: "Reports a problem with homespun itself to the relay operator, and lists what this agent has already reported. A report is the operator's only visibility into a failure that happened inside an agent's session, so an unreported one is a failure nobody can fix.\n\nThe channel covers homespun's own behaviour: a 5xx, or an error code the guide does not describe; a disagreement between documented and observed behaviour; something the tool surface cannot express, such as a missing capability or a schema that contradicts itself; an app misbehaving in a way that traces back to the platform (the bridge, the runtime, serving, the data API) rather than to authored HTML; or a guide that was wrong, ambiguous or silent.\n\nOutside its scope: the human's own task; bugs in an app the agent authored; presentation preferences, which belong in `taste`; the human's own configuration, such as a missing API key or the wrong account; and a 4xx caused by the agent's own arguments, except where the error message itself was misleading, which is a documentation problem best filed as a `note`.\n\nDuplicates cost the operator triage rather than adding signal. Action `list` returns this agent's own submissions, newest first, so a failure already recorded needs no second row: one report covers one distinct failure, however many times it was retried.\n\nThe operator sees the row and not the session, so a bare \"deploy failed\" is not actionable. An actionable `message` carries the surface (mcp, cli, relay or app-runtime); where it happened (the tool or route); the skill version, from the `<!-- homespun skill vX.Y.Z -->` comment at the top of the guide; what was expected, in one line; what was observed, in one line carrying the exact error code and message; and the minimal steps or arguments that reproduce it.\n\n`type` is bug for something broken, feature for something missing, note for a rough edge or a confusing doc. `app_id` scopes a report to one app. There is no reply channel, so a report is not a route to an answer. Actions: create files one report; list returns this agent's own submissions, newest first, paginated by `before`.",
|
|
1765
2113
|
inputSchema: feedbackShape,
|
|
1766
2114
|
// Consolidated tool: read action (list) + a side-effecting one (create
|
|
1767
2115
|
// submits feedback to the relay operator). Hint reflects the write action.
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.6.
|
|
1
|
+
export declare const VERSION = "1.6.45";
|
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.45",
|
|
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.45",
|
|
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.45",
|
|
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.45",
|
|
14
14
|
"transport": {
|
|
15
15
|
"type": "stdio"
|
|
16
16
|
},
|