sealnet-mcp 0.3.0 → 0.4.0
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 +3 -2
- package/dist/cli.cjs +360 -37
- package/dist/cli.js +360 -37
- package/dist/index.cjs +360 -37
- package/dist/index.d.cts +91 -8
- package/dist/index.d.ts +91 -8
- package/dist/index.js +360 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Model Context Protocol (MCP) server for **SEAL**.
|
|
|
9
9
|
When an MCP-connected AI agent (Claude Desktop, Cursor, Cline, etc.) creates a SEAL via this server:
|
|
10
10
|
|
|
11
11
|
- **handoff mode (default)**: the share link (the key is in its fragment, there is no password) is delivered to the human user via **system clipboard** (primary) or **0o600 local file** (fallback when clipboard is unavailable — headless, SSH, WSL without display). The `tool_result` returned to the model contains **only** an opaque handle and a human-readable receipt. The link is never written into the conversation; on the file channel the receipt names the file, and an agent that can read files on that machine can read it.
|
|
12
|
-
- **forward mode (explicit opt-in)**: model receives `{ handle, share_url, expires_at, max_reads, mode }` in `tool_result`; the link opens only with the recipient's X25519 key. Requires short TTL (≤30m), `max_reads=1`, mandatory `to` (recipient
|
|
12
|
+
- **forward mode (explicit opt-in)**: model receives `{ handle, share_url, expires_at, max_reads, mode, delivered }` in `tool_result`; the link opens only with the recipient's X25519 key and also lands in the recipient's inbox (`seal_inbox`). Requires short TTL (≤30m), `max_reads=1`, mandatory `to` (recipient address). Every forward is audit-logged in the encrypted state file.
|
|
13
13
|
|
|
14
14
|
## Tool surface
|
|
15
15
|
|
|
@@ -17,7 +17,8 @@ When an MCP-connected AI agent (Claude Desktop, Cursor, Cline, etc.) creates a S
|
|
|
17
17
|
|------|---------|
|
|
18
18
|
| `seal_share(path, mode='handoff'\|'forward', expire?, max_reads?, tier?)` / `seal_share(payment)` | Create a SEAL from a local file or folder (sent as one ZIP), streamed from disk. Default: handoff; without `expire`, 5m, or 1d for a handoff over 100 MB. Over 1 GB the user pays once by card (smallest tier that fits, or `tier`): the payment page goes through URL-mode elicitation, or comes back as `payment_url` and a second call with `payment` finishes the upload into the same seal |
|
|
19
19
|
| `seal_request(what, kind='any'\|'secret'\|'file', where_url?, wait=true)` / `seal_request(request)` | Ask the user for a file or a secret without it passing through the chat. On the user's computer the `seal` CLI draws the system dialog (`seal request --local --json`); otherwise the user answers at `seal.net/i/<id>`, sealed to this server's key, shown with three emoji of that key. Returns the path of a 0600 file, never the value |
|
|
20
|
-
| `seal_open(url, mode='metadata'\|'file'\|'inline', dir?)` | Read metadata (default — no download, no counter consumption), save to disk (`file`: streamed, verified before it gets its name, 0600, returns paths) or inline parse (whitelisted MIMEs only, ≤100 KB, refuses secrets, HTML and binary) |
|
|
20
|
+
| `seal_open(url\|item, mode='metadata'\|'file'\|'inline', dir?)` | Read metadata (default — no download, no counter consumption), save to disk (`file`: streamed, verified before it gets its name, 0600, returns paths) or inline parse (whitelisted MIMEs only, ≤100 KB, refuses secrets, HTML and binary). `item` is an inbox handle instead of a link |
|
|
21
|
+
| `seal_inbox(wait?)` | Seals other agents or people sent to this server's key (its address): handles, the sender's three emoji and name, never a link. `seal_share mode=forward to=<address>` puts a link there; `wait` (0 to 300 s) waits for something new |
|
|
21
22
|
| `seal_list()` | List SEALs created by **this MCP server only** (scope: `state.json`; never reveals user's CLI seals) |
|
|
22
23
|
| `seal_revoke(handle)` | Revoke by opaque handle (model never learns raw `seal_id`) |
|
|
23
24
|
|
package/dist/cli.cjs
CHANGED
|
@@ -2890,9 +2890,60 @@ var limits_default = {
|
|
|
2890
2890
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
2891
2891
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
2892
2892
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
2893
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
2893
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
2894
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
2895
|
+
INBOX_MAX_ITEMS: 200
|
|
2894
2896
|
}
|
|
2895
2897
|
};
|
|
2898
|
+
var seal_address_v1_default = {
|
|
2899
|
+
version: 1,
|
|
2900
|
+
$comment: "Address of a key: who a seal is for, without a URL (audit/SPEC-ADDRESS.md). Numbers live in limits.json, error codes in error_codes.json.",
|
|
2901
|
+
address: {
|
|
2902
|
+
key: "X25519 public key, 32 bytes",
|
|
2903
|
+
text: "base64url without padding, 43 characters",
|
|
2904
|
+
emoji: "fingerprint_emoji.json over sha256(key)[0..3], the same picture as intake",
|
|
2905
|
+
inbox_hash: "sha256(key), 32 bytes: the server finds an inbox by it"
|
|
2906
|
+
},
|
|
2907
|
+
seed: {
|
|
2908
|
+
env: "SEAL_SEED",
|
|
2909
|
+
text: "base64url without padding of the 32-byte X25519 secret, 43 characters",
|
|
2910
|
+
same_as: [
|
|
2911
|
+
"sealnet-mcp state.identity.x25519_secret_b64",
|
|
2912
|
+
"seal identity.v1.json x25519_secret_b64url",
|
|
2913
|
+
"sealnet (npm) KeyPair.secretKey",
|
|
2914
|
+
"sealnet (PyPI) keygen().secret_key"
|
|
2915
|
+
]
|
|
2916
|
+
},
|
|
2917
|
+
scopes: {
|
|
2918
|
+
share_link: "seal_share",
|
|
2919
|
+
owner_copy: "seal_owner",
|
|
2920
|
+
address_proof: "seal_address_proof"
|
|
2921
|
+
},
|
|
2922
|
+
proof: {
|
|
2923
|
+
header_address: "X-Seal-Address",
|
|
2924
|
+
header_proof: "X-Seal-Proof",
|
|
2925
|
+
proof_text: "<ts>.<token>: ts is the decimal Unix second from the challenge, token base64url of 32 bytes",
|
|
2926
|
+
challenge: "sealed box of the 32-byte token to the address with scope (address_proof, decimal ts): ephemeral_pub(32) || nonce(12) || ciphertext(48), base64url, 123 characters",
|
|
2927
|
+
token: "opaque to clients: unwrap it and send it back"
|
|
2928
|
+
},
|
|
2929
|
+
items: {
|
|
2930
|
+
delivery: {
|
|
2931
|
+
id: "s.<seal_id>.<share_code>",
|
|
2932
|
+
is: "a share link addressed to this key; fragment = its key wrapped to the address (x25519:, targeted_x25519_v1, scope share_link)"
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
request_kinds: ["secret", "file", "any"],
|
|
2936
|
+
vectors: [
|
|
2937
|
+
{
|
|
2938
|
+
comment: "generated once by backend_crypto.sealed_box.seal_key_for_recipient; every client must unwrap it",
|
|
2939
|
+
secret: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc",
|
|
2940
|
+
address: "E75P6uryBMf9M1j8nAByGIHRdCeBKCJ-xnTzf3_pe20",
|
|
2941
|
+
ts: 179e7,
|
|
2942
|
+
challenge: "4Oel-UMfgP_1dBhmf13yZiujhPDouCIQ6sSx1nY1qy-vyzFqDqb9jARbJ0e5A8vv4EDELDS5OPqBQMcsmDA-C_lXO95y_CVUyYtSu-1fltsy5ZihOFDp0T-C9Ro",
|
|
2943
|
+
token: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"
|
|
2944
|
+
}
|
|
2945
|
+
]
|
|
2946
|
+
};
|
|
2896
2947
|
var upload_default = {
|
|
2897
2948
|
version: 1,
|
|
2898
2949
|
upload: {
|
|
@@ -3224,13 +3275,24 @@ var SealApi = class _SealApi {
|
|
|
3224
3275
|
});
|
|
3225
3276
|
return { etag: resp.headers.get("ETag") };
|
|
3226
3277
|
}
|
|
3227
|
-
/**
|
|
3278
|
+
/**
|
|
3279
|
+
* `POST /seal/{id}/share` — mint a share link, returns its code. With an
|
|
3280
|
+
* address the link also lands in that key's inbox, in the same request.
|
|
3281
|
+
*/
|
|
3228
3282
|
async createShareLink(sealId, ownerToken, encryptedBlobB64, opts = {}) {
|
|
3283
|
+
const headers = _SealApi.ownerHeaders(ownerToken, true);
|
|
3284
|
+
const body = { encrypted_blob: encryptedBlobB64 };
|
|
3285
|
+
if ("to" in opts) {
|
|
3286
|
+
body.to = opts.to;
|
|
3287
|
+
body.fragment = opts.fragment;
|
|
3288
|
+
if (opts.label) body.label = opts.label;
|
|
3289
|
+
if (opts.sender) Object.assign(headers, proofHeaders(opts.sender));
|
|
3290
|
+
}
|
|
3229
3291
|
const resp = await this.request(`/seal/${encode(sealId)}/share`, {
|
|
3230
3292
|
what: `POST /seal/${sealId}/share`,
|
|
3231
3293
|
method: "POST",
|
|
3232
|
-
headers
|
|
3233
|
-
body: JSON.stringify(
|
|
3294
|
+
headers,
|
|
3295
|
+
body: JSON.stringify(body),
|
|
3234
3296
|
signal: opts.signal
|
|
3235
3297
|
});
|
|
3236
3298
|
const data = await resp.json();
|
|
@@ -3413,8 +3475,57 @@ var SealApi = class _SealApi {
|
|
|
3413
3475
|
});
|
|
3414
3476
|
return await resp.json();
|
|
3415
3477
|
}
|
|
3478
|
+
// ---------------------------------------------------------------------------
|
|
3479
|
+
// Inbox (SPEC-ADDRESS F2): the share links addressed to a key
|
|
3480
|
+
// ---------------------------------------------------------------------------
|
|
3481
|
+
/** `POST /intake/proof` — a challenge sealed to `address`. */
|
|
3482
|
+
async requestAddressProof(address, opts = {}) {
|
|
3483
|
+
const resp = await this.request("/intake/proof", {
|
|
3484
|
+
what: "POST /intake/proof",
|
|
3485
|
+
method: "POST",
|
|
3486
|
+
headers: { "Content-Type": "application/json" },
|
|
3487
|
+
body: JSON.stringify({ address }),
|
|
3488
|
+
signal: opts.signal
|
|
3489
|
+
});
|
|
3490
|
+
return await resp.json();
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* `GET /intake?to=` — every live item. `waitSeconds` holds the answer
|
|
3494
|
+
* while the inbox is empty, or, with `seen`, while its version is still
|
|
3495
|
+
* `seen`; one call waits at most `INTAKE_WAIT_MAX_SECONDS`, longer waits
|
|
3496
|
+
* are the caller's loop.
|
|
3497
|
+
*/
|
|
3498
|
+
async listInbox(proof, opts = {}) {
|
|
3499
|
+
const params = new URLSearchParams({ to: proof.address });
|
|
3500
|
+
if (opts.waitSeconds) {
|
|
3501
|
+
params.set("wait", String(Math.min(opts.waitSeconds, limits_default.limits.INTAKE_WAIT_MAX_SECONDS)));
|
|
3502
|
+
}
|
|
3503
|
+
if (opts.seen) params.set("seen", opts.seen);
|
|
3504
|
+
const resp = await this.request(`/intake?${params.toString()}`, {
|
|
3505
|
+
what: "GET /intake",
|
|
3506
|
+
headers: proofHeaders(proof),
|
|
3507
|
+
signal: opts.signal
|
|
3508
|
+
});
|
|
3509
|
+
return await resp.json();
|
|
3510
|
+
}
|
|
3511
|
+
/** `DELETE /intake?to=&item=` — take one item out; the link keeps working. */
|
|
3512
|
+
async dismissInboxItem(itemId, proof, opts = {}) {
|
|
3513
|
+
const params = new URLSearchParams({ to: proof.address, item: itemId });
|
|
3514
|
+
await this.request(`/intake?${params.toString()}`, {
|
|
3515
|
+
what: "DELETE /intake",
|
|
3516
|
+
method: "DELETE",
|
|
3517
|
+
headers: proofHeaders(proof),
|
|
3518
|
+
signal: opts.signal
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3416
3521
|
};
|
|
3417
3522
|
var NOT_READY_DEFAULT_RETRY_AFTER_S = 3;
|
|
3523
|
+
function proofHeaders(proof) {
|
|
3524
|
+
return {
|
|
3525
|
+
[seal_address_v1_default.proof.header_address]: proof.address,
|
|
3526
|
+
[seal_address_v1_default.proof.header_proof]: proof.proof
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3418
3529
|
function abortableSleep(ms, signal) {
|
|
3419
3530
|
return new Promise((resolve5, reject) => {
|
|
3420
3531
|
if (signal?.aborted) {
|
|
@@ -6179,6 +6290,7 @@ function parseUrlType(pathname) {
|
|
|
6179
6290
|
var TARGETED_PREFIX_V1 = "x25519:";
|
|
6180
6291
|
var TARGETED_PREFIX_V2_RESERVED = "x25519v2:";
|
|
6181
6292
|
var SCOPE_TYPE = "seal_share";
|
|
6293
|
+
var ADDRESS_PROOF_SCOPE = "seal_address_proof";
|
|
6182
6294
|
var X25519_KEY_BYTES = 32;
|
|
6183
6295
|
var LINK_KEY_BYTES2 = 32;
|
|
6184
6296
|
var PACKED_BYTES_V1 = 92;
|
|
@@ -6285,6 +6397,11 @@ async function unwrapTargetedFragment(fragment, recipientSecret, sealId) {
|
|
|
6285
6397
|
}
|
|
6286
6398
|
return linkKey;
|
|
6287
6399
|
}
|
|
6400
|
+
async function proveAddress(secret, challenge, ts) {
|
|
6401
|
+
const wasm3 = await ensureWasm();
|
|
6402
|
+
const token = wasm3.wasm_sealbox_unwrap(base64urlToBytes(challenge), secret, ADDRESS_PROOF_SCOPE, String(ts));
|
|
6403
|
+
return `${ts}.${bytesToBase64url(token)}`;
|
|
6404
|
+
}
|
|
6288
6405
|
var fingerprint_emoji_default = {
|
|
6289
6406
|
version: 1,
|
|
6290
6407
|
name: "FINGERPRINT_EMOJI",
|
|
@@ -6744,6 +6861,36 @@ async function waitForIntake(api, intakeId, opts = {}) {
|
|
|
6744
6861
|
if (view.status === "fulfilled" && view.share_path) return view.share_path;
|
|
6745
6862
|
}
|
|
6746
6863
|
}
|
|
6864
|
+
var PROOF_MARGIN_MS = 3e4;
|
|
6865
|
+
var proofs = /* @__PURE__ */ new Map();
|
|
6866
|
+
async function addressProof(api, secret, opts = {}) {
|
|
6867
|
+
const address = bytesToBase64url(await recipientPublicKey(secret));
|
|
6868
|
+
const key = `${api.baseUrl} ${address}`;
|
|
6869
|
+
const kept = proofs.get(key);
|
|
6870
|
+
if (!opts.fresh && kept && kept.until > Date.now()) return kept.proof;
|
|
6871
|
+
const challenge = await api.requestAddressProof(address, opts);
|
|
6872
|
+
const proof = { address, proof: await proveAddress(secret, challenge.challenge, challenge.ts) };
|
|
6873
|
+
proofs.set(key, { proof, until: Date.parse(challenge.expires_at) - PROOF_MARGIN_MS });
|
|
6874
|
+
return proof;
|
|
6875
|
+
}
|
|
6876
|
+
async function withAddressProof(api, secret, signal, call) {
|
|
6877
|
+
const request = signal ? { signal } : {};
|
|
6878
|
+
try {
|
|
6879
|
+
return await call(await addressProof(api, secret, request));
|
|
6880
|
+
} catch (err2) {
|
|
6881
|
+
if (!(err2 instanceof BackendError && err2.appCode === "intake.proof_required")) throw err2;
|
|
6882
|
+
return call(await addressProof(api, secret, { ...request, fresh: true }));
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
async function readInbox(api, secret, opts = {}) {
|
|
6886
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.listInbox(proof, opts));
|
|
6887
|
+
}
|
|
6888
|
+
async function removeFromInbox(api, secret, itemId, opts = {}) {
|
|
6889
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.dismissInboxItem(itemId, proof, opts));
|
|
6890
|
+
}
|
|
6891
|
+
function inboxItemUrl(site, item) {
|
|
6892
|
+
return `${site.replace(/\/+$/, "")}${item.path}#${item.fragment}`;
|
|
6893
|
+
}
|
|
6747
6894
|
async function describeFile(file, fileKeys) {
|
|
6748
6895
|
const keyB64 = file.sha256_hex ? fileKeys[file.sha256_hex] : void 0;
|
|
6749
6896
|
const base = { id: file.id, size: file.size, sha256Hex: file.sha256_hex };
|
|
@@ -11664,7 +11811,7 @@ var seal_mcp_v1_default = {
|
|
|
11664
11811
|
progress: "When a `tools/call` carries `_meta.progressToken`, `seal_share` and `seal_open mode=file` send `notifications/progress` with `progress` = ciphertext bytes done (upload: confirmed by storage) and `total` = ciphertext bytes in all; at most one per second, `progress` strictly increasing, the last one at `progress = total`."
|
|
11665
11812
|
},
|
|
11666
11813
|
server_instructions: {
|
|
11667
|
-
text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files). Never read or print a SEAL handoff file.",
|
|
11814
|
+
text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files). Files sent to your key arrive in seal_inbox; open them with seal_open item. Never read or print a SEAL handoff file.",
|
|
11668
11815
|
note: "The `instructions` of the `initialize` result (SPEC-AGENTS \xA78.1). Hosts that defer MCP tools behind a tool search (Claude Code) show the model only tool names until it searches, but put these instructions into the system prompt: this is the one text a model reads before it decides to look for a tool, or reads the file it was asked to send."
|
|
11669
11816
|
},
|
|
11670
11817
|
tools: {
|
|
@@ -11732,7 +11879,8 @@ var seal_mcp_v1_default = {
|
|
|
11732
11879
|
share_url: { type: "string", format: "uri", description: "`targeted_x25519_v1` link: the wrapped key is in the fragment, useless without the recipient's X25519 secret; there is no separate password." },
|
|
11733
11880
|
expires_at: { type: "string", format: "date-time" },
|
|
11734
11881
|
max_reads: { type: "integer", const: 1 },
|
|
11735
|
-
mode: { type: "string", const: "forward" }
|
|
11882
|
+
mode: { type: "string", const: "forward" },
|
|
11883
|
+
delivered: { type: "boolean", description: "The link also lies in the recipient's inbox (SPEC-ADDRESS F2): the server keeps its wrapped key in the link's row, and the holder of the key finds it with seal_inbox without being handed the URL." }
|
|
11736
11884
|
},
|
|
11737
11885
|
policy: {
|
|
11738
11886
|
max_expire_minutes: 30,
|
|
@@ -11800,9 +11948,11 @@ var seal_mcp_v1_default = {
|
|
|
11800
11948
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
11801
11949
|
annotations_note: "Set on the tool, not per mode: `inline` and `file` consume an open, so the tool as a whole is not read-only.",
|
|
11802
11950
|
input: {
|
|
11803
|
-
|
|
11951
|
+
one_of: ["url", "item"],
|
|
11952
|
+
one_of_note: "Exactly one of `url` and `item`; `server.ts` takes both as optional and the handler refuses anything else with `url_invalid`.",
|
|
11804
11953
|
properties: {
|
|
11805
11954
|
url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
|
|
11955
|
+
item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
|
|
11806
11956
|
mode: { type: "string", enum: ["metadata", "inline", "file"], default: "metadata", description: "`metadata` reads nothing; `file` saves to disk; `inline` returns small text." },
|
|
11807
11957
|
dir: { type: "string", description: "mode=file: directory to save into; default ~/Downloads, created if missing. An existing file is never overwritten: the new one becomes `name (1).ext`." }
|
|
11808
11958
|
}
|
|
@@ -11859,6 +12009,40 @@ var seal_mcp_v1_default = {
|
|
|
11859
12009
|
_binary_default: "inline_blocked.binary"
|
|
11860
12010
|
}
|
|
11861
12011
|
},
|
|
12012
|
+
seal_inbox: {
|
|
12013
|
+
title: "Check the inbox",
|
|
12014
|
+
description: "Seals other agents or people sent to your key (your address). Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
|
|
12015
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12016
|
+
input: {
|
|
12017
|
+
properties: {
|
|
12018
|
+
wait: { type: "integer", minimum: 0, maximum: 300, default: 0, description: "Seconds to wait for an item this server has not seen yet; 0 returns the inbox now." }
|
|
12019
|
+
}
|
|
12020
|
+
},
|
|
12021
|
+
flow: "`GET /intake?to=<this server's address>` under the proof of the key (seal_address.v1.json proof), cached until 30 s before it expires. The first call in a process returns the list at once; later ones long-poll by 25 s with `seen` = the version of the last answer until an id this process has not seen arrives or `wait` runs out. Handles, rows and the version live in process memory.",
|
|
12022
|
+
output: {
|
|
12023
|
+
type: "object",
|
|
12024
|
+
required: ["address_emoji", "items"],
|
|
12025
|
+
properties: {
|
|
12026
|
+
address_emoji: { type: "string", description: "Three emoji of this server's address (`fingerprint_emoji.json`)." },
|
|
12027
|
+
items: {
|
|
12028
|
+
type: "array",
|
|
12029
|
+
items: {
|
|
12030
|
+
type: "object",
|
|
12031
|
+
required: ["handle", "from", "label", "received_at", "expires_at"],
|
|
12032
|
+
additionalProperties: false,
|
|
12033
|
+
properties: {
|
|
12034
|
+
handle: { type: "string", pattern: "^in_", description: "`in_` + the inbox item id; pass as seal_open `item`." },
|
|
12035
|
+
from: { type: ["string", "null"], description: "Three emoji of the sender's address when the server verified it, else null." },
|
|
12036
|
+
label: { type: ["string", "null"], description: "What the sender called itself (an agent's name, `sealnet`), in the clear: never content." },
|
|
12037
|
+
received_at: { type: "string", format: "date-time" },
|
|
12038
|
+
expires_at: { type: "string", format: "date-time" }
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
},
|
|
12043
|
+
MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
|
|
12044
|
+
}
|
|
12045
|
+
},
|
|
11862
12046
|
seal_list: {
|
|
11863
12047
|
title: "List SEALs created via THIS MCP server",
|
|
11864
12048
|
description: "Seals you created through this server: opaque handles with status and expiry. Seals made with the CLI or the desktop client live elsewhere and are not listed.",
|
|
@@ -12182,6 +12366,10 @@ var seal_mcp_v1_default = {
|
|
|
12182
12366
|
share_blob_corrupt: "Share blob decoded but a file_key is not valid base64url or has the wrong length (32 bytes expected). Backend bug or share-blob tampering.",
|
|
12183
12367
|
share_blob_failed: "`encryptShareBlob` threw during seal_share mint (Web Crypto failure or input shape bug). Caller-facing in seal_share path only.",
|
|
12184
12368
|
share_link_failed: "POST /seal/{id}/share returned an error (network, 5xx, owner_token mismatch).",
|
|
12369
|
+
inbox_proof_failed: "seal_inbox / seal_open item / seal_share forward: the server refused the proof of this server's key (`intake.proof_required`) even after one fresh challenge.",
|
|
12370
|
+
inbox_full: "seal_share forward: the recipient's inbox holds `limits.json::INBOX_MAX_ITEMS` live items from other senders (`intake.inbox_full`). The seal is revoked; retry later.",
|
|
12371
|
+
inbox_item_gone: "seal_open item: the handle is not in this process's inbox and not in a fresh read of it \u2014 the seal or link was revoked or expired, or the item was removed.",
|
|
12372
|
+
inbox_failed: "seal_inbox / seal_open item: reading or cleaning the inbox failed for a reason other than the proof (network, 5xx, rate limit); the message has the status. Safe to retry.",
|
|
12185
12373
|
targeted_fragment_invalid: "seal_open of `x25519:` URL: fragment is malformed base64url, wrong packed length, or AEAD-failed for a non-secret reason. Distinct from `targeted_decrypt_failed` which is the wrong-recipient case.",
|
|
12186
12374
|
targeted_decrypt_failed: "seal_open of `x25519:` URL: AEAD verification failed under THIS device's identity. The URL was not addressed to this MCP's pubkey (or was tampered, or seal_id from path-segment was substituted). Recipient should ask the sender to re-mint with `--to <our pubkey>`.",
|
|
12187
12375
|
auth_required: "Backend returned 401/403 because the seal has OTP / NDA / login gating; interactive auth is not supported in MCP. Open the URL in a browser.",
|
|
@@ -12791,6 +12979,72 @@ var ToolError = class extends Error {
|
|
|
12791
12979
|
}
|
|
12792
12980
|
};
|
|
12793
12981
|
|
|
12982
|
+
// src/tools/inbox.ts
|
|
12983
|
+
var HANDLE_PREFIX2 = "in_";
|
|
12984
|
+
function inboxError(err2) {
|
|
12985
|
+
if (!(err2 instanceof BackendError)) return err2;
|
|
12986
|
+
if (err2.appCode === "intake.proof_required") {
|
|
12987
|
+
return new ToolError("inbox_proof_failed", `the server refused the proof of this server's key: ${err2.message}`, err2);
|
|
12988
|
+
}
|
|
12989
|
+
return new ToolError("inbox_failed", `the inbox failed (${err2.status}): ${err2.message}`, err2);
|
|
12990
|
+
}
|
|
12991
|
+
function memoryOf(ctx) {
|
|
12992
|
+
if (!ctx.inbox) throw new ToolError("internal_inconsistency", "this server keeps no inbox memory");
|
|
12993
|
+
return ctx.inbox;
|
|
12994
|
+
}
|
|
12995
|
+
async function sealInbox(ctx, input = {}) {
|
|
12996
|
+
const memory = memoryOf(ctx);
|
|
12997
|
+
const deadline = Date.now() + (memory.version === null ? 0 : (input.wait ?? 0) * 1e3);
|
|
12998
|
+
let seen = memory.version ?? void 0;
|
|
12999
|
+
let inbox;
|
|
13000
|
+
for (; ; ) {
|
|
13001
|
+
const left = Math.ceil((deadline - Date.now()) / 1e3);
|
|
13002
|
+
try {
|
|
13003
|
+
inbox = await readInbox(ctx.client, ctx.identity.secret, left > 0 ? { waitSeconds: left, seen } : {});
|
|
13004
|
+
} catch (err2) {
|
|
13005
|
+
throw inboxError(err2);
|
|
13006
|
+
}
|
|
13007
|
+
const arrived = inbox.items.some((item) => !memory.items.has(HANDLE_PREFIX2 + item.id));
|
|
13008
|
+
if (left <= 0 || arrived || Date.now() >= deadline) break;
|
|
13009
|
+
seen = inbox.version;
|
|
13010
|
+
}
|
|
13011
|
+
memory.items.clear();
|
|
13012
|
+
for (const item of inbox.items) memory.items.set(HANDLE_PREFIX2 + item.id, item);
|
|
13013
|
+
memory.version = inbox.version;
|
|
13014
|
+
return {
|
|
13015
|
+
addressEmoji: keyEmoji(ctx.identity.public),
|
|
13016
|
+
items: inbox.items.map((item) => ({
|
|
13017
|
+
handle: HANDLE_PREFIX2 + item.id,
|
|
13018
|
+
from: item.sender === null ? null : keyEmoji(base64urlToBytes(item.sender)),
|
|
13019
|
+
label: item.label,
|
|
13020
|
+
receivedAt: item.at,
|
|
13021
|
+
expiresAt: item.expires_at
|
|
13022
|
+
}))
|
|
13023
|
+
};
|
|
13024
|
+
}
|
|
13025
|
+
async function inboxItem(ctx, handle) {
|
|
13026
|
+
const memory = memoryOf(ctx);
|
|
13027
|
+
if (!memory.items.has(handle)) await sealInbox(ctx, {});
|
|
13028
|
+
const item = memory.items.get(handle);
|
|
13029
|
+
if (!item) {
|
|
13030
|
+
throw new ToolError(
|
|
13031
|
+
"inbox_item_gone",
|
|
13032
|
+
`${handle} is not in the inbox: the seal or link was revoked or expired, or the item was removed`
|
|
13033
|
+
);
|
|
13034
|
+
}
|
|
13035
|
+
return item;
|
|
13036
|
+
}
|
|
13037
|
+
async function forgetInboxItem(ctx, handle) {
|
|
13038
|
+
const item = memoryOf(ctx).items.get(handle);
|
|
13039
|
+
if (!item) return;
|
|
13040
|
+
try {
|
|
13041
|
+
await removeFromInbox(ctx.client, ctx.identity.secret, item.id);
|
|
13042
|
+
} catch (err2) {
|
|
13043
|
+
throw inboxError(err2);
|
|
13044
|
+
}
|
|
13045
|
+
memoryOf(ctx).items.delete(handle);
|
|
13046
|
+
}
|
|
13047
|
+
|
|
12794
13048
|
// src/tools/list.ts
|
|
12795
13049
|
var SINGLE_FILE_COUNT = 1;
|
|
12796
13050
|
async function sealList(ctx, input = {}) {
|
|
@@ -12882,11 +13136,28 @@ async function download(ctx, sealId, file) {
|
|
|
12882
13136
|
return resp.body;
|
|
12883
13137
|
}
|
|
12884
13138
|
async function sealOpen(ctx, input) {
|
|
13139
|
+
if (input.url === void 0 === (input.item === void 0)) {
|
|
13140
|
+
throw new ToolError("url_invalid", "give exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
|
|
13141
|
+
}
|
|
13142
|
+
const recipient = { recipientSecret: ctx.identity.secret };
|
|
13143
|
+
if (input.url !== void 0) return openSeal(ctx, input, () => openShareLink(ctx.client, input.url, recipient));
|
|
13144
|
+
const handle = input.item;
|
|
13145
|
+
const item = await inboxItem(ctx, handle);
|
|
13146
|
+
try {
|
|
13147
|
+
return await openSeal(ctx, input, () => openShareLink(ctx.client, inboxItemUrl(resolvePublicHost(), item), recipient));
|
|
13148
|
+
} catch (err2) {
|
|
13149
|
+
if (err2 instanceof ToolError && (err2.code === "seal_revoked" || err2.code === "share_link_revoked")) {
|
|
13150
|
+
await forgetInboxItem(ctx, handle).catch(() => void 0);
|
|
13151
|
+
}
|
|
13152
|
+
throw err2;
|
|
13153
|
+
}
|
|
13154
|
+
}
|
|
13155
|
+
async function openSeal(ctx, input, read) {
|
|
12885
13156
|
const mode = input.mode ?? "metadata";
|
|
12886
13157
|
const handle = generateHandle();
|
|
12887
13158
|
let seal;
|
|
12888
13159
|
try {
|
|
12889
|
-
seal = await
|
|
13160
|
+
seal = await read();
|
|
12890
13161
|
} catch (err2) {
|
|
12891
13162
|
throw asToolError(err2);
|
|
12892
13163
|
}
|
|
@@ -13234,7 +13505,9 @@ var limits_default2 = {
|
|
|
13234
13505
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
13235
13506
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
13236
13507
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
13237
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
13508
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
13509
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
13510
|
+
INBOX_MAX_ITEMS: 200
|
|
13238
13511
|
}
|
|
13239
13512
|
};
|
|
13240
13513
|
|
|
@@ -13752,17 +14025,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13752
14025
|
);
|
|
13753
14026
|
}
|
|
13754
14027
|
const shareBlobB64 = bytesToBase64url(shareBlobBytes);
|
|
13755
|
-
let shareCode;
|
|
13756
|
-
try {
|
|
13757
|
-
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13758
|
-
} catch (err2) {
|
|
13759
|
-
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13760
|
-
throw new ToolError(
|
|
13761
|
-
"share_link_failed",
|
|
13762
|
-
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13763
|
-
err2
|
|
13764
|
-
);
|
|
13765
|
-
}
|
|
13766
14028
|
let fragment;
|
|
13767
14029
|
if (mode === "forward") {
|
|
13768
14030
|
if (recipientPub === null) {
|
|
@@ -13792,6 +14054,29 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13792
14054
|
} else {
|
|
13793
14055
|
fragment = bytesToBase64url(shareLinkKey);
|
|
13794
14056
|
}
|
|
14057
|
+
if (mode === "forward") await journalDelivering(ctx, handle, op);
|
|
14058
|
+
let shareCode;
|
|
14059
|
+
try {
|
|
14060
|
+
shareCode = mode === "forward" && recipientPub !== null ? await withAddressProof(
|
|
14061
|
+
ctx.client,
|
|
14062
|
+
ctx.identity.secret,
|
|
14063
|
+
void 0,
|
|
14064
|
+
(sender) => ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64, {
|
|
14065
|
+
to: bytesToBase64url(recipientPub),
|
|
14066
|
+
fragment,
|
|
14067
|
+
label: deps.label?.slice(0, 80),
|
|
14068
|
+
sender
|
|
14069
|
+
})
|
|
14070
|
+
) : await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
14071
|
+
} catch (err2) {
|
|
14072
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14073
|
+
const appCode = err2 instanceof BackendError ? err2.appCode : void 0;
|
|
14074
|
+
throw new ToolError(
|
|
14075
|
+
appCode === "intake.inbox_full" ? "inbox_full" : appCode === "intake.proof_required" ? "inbox_proof_failed" : "share_link_failed",
|
|
14076
|
+
`POST /seal/${uploaded.sealId}/share failed: ${messageOf3(err2)} (${outcome})`,
|
|
14077
|
+
err2
|
|
14078
|
+
);
|
|
14079
|
+
}
|
|
13795
14080
|
const host = resolvePublicHost(deps.publicHost);
|
|
13796
14081
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13797
14082
|
if (mode === "forward") {
|
|
@@ -13808,21 +14093,12 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13808
14093
|
shareUrl,
|
|
13809
14094
|
expiresAt,
|
|
13810
14095
|
maxReads: FORWARD_MAX_READS_FORCED,
|
|
13811
|
-
mode: "forward"
|
|
14096
|
+
mode: "forward",
|
|
14097
|
+
delivered: true
|
|
13812
14098
|
};
|
|
13813
14099
|
}
|
|
13814
|
-
op.step = "delivering";
|
|
13815
14100
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13816
|
-
|
|
13817
|
-
await journal(ctx, handle, op);
|
|
13818
|
-
} catch (err2) {
|
|
13819
|
-
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13820
|
-
throw new ToolError(
|
|
13821
|
-
"state_persist_failed",
|
|
13822
|
-
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13823
|
-
err2
|
|
13824
|
-
);
|
|
13825
|
-
}
|
|
14101
|
+
await journalDelivering(ctx, handle, op);
|
|
13826
14102
|
let handoff;
|
|
13827
14103
|
try {
|
|
13828
14104
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
@@ -13857,6 +14133,19 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13857
14133
|
function messageOf3(err2) {
|
|
13858
14134
|
return err2 instanceof Error ? err2.message : String(err2);
|
|
13859
14135
|
}
|
|
14136
|
+
async function journalDelivering(ctx, handle, op) {
|
|
14137
|
+
op.step = "delivering";
|
|
14138
|
+
try {
|
|
14139
|
+
await journal(ctx, handle, op);
|
|
14140
|
+
} catch (err2) {
|
|
14141
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14142
|
+
throw new ToolError(
|
|
14143
|
+
"state_persist_failed",
|
|
14144
|
+
`could not journal the delivery attempt before the link could reach anyone: ${messageOf3(err2)} (${outcome})`,
|
|
14145
|
+
err2
|
|
14146
|
+
);
|
|
14147
|
+
}
|
|
14148
|
+
}
|
|
13860
14149
|
async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, session }) {
|
|
13861
14150
|
const tier = prepared.tier;
|
|
13862
14151
|
const sealId = op.seal_id;
|
|
@@ -13948,7 +14237,8 @@ function shareOutputToWire(out) {
|
|
|
13948
14237
|
share_url: out.shareUrl,
|
|
13949
14238
|
expires_at: out.expiresAt,
|
|
13950
14239
|
max_reads: out.maxReads,
|
|
13951
|
-
mode: out.mode
|
|
14240
|
+
mode: out.mode,
|
|
14241
|
+
delivered: out.delivered
|
|
13952
14242
|
};
|
|
13953
14243
|
}
|
|
13954
14244
|
const wire = {
|
|
@@ -14015,6 +14305,18 @@ function openOutputToWire(out) {
|
|
|
14015
14305
|
expires_at: out.expiresAt
|
|
14016
14306
|
};
|
|
14017
14307
|
}
|
|
14308
|
+
function inboxOutputToWire(out) {
|
|
14309
|
+
return {
|
|
14310
|
+
address_emoji: out.addressEmoji,
|
|
14311
|
+
items: out.items.map((item) => ({
|
|
14312
|
+
handle: item.handle,
|
|
14313
|
+
from: item.from,
|
|
14314
|
+
label: item.label,
|
|
14315
|
+
received_at: item.receivedAt,
|
|
14316
|
+
expires_at: item.expiresAt
|
|
14317
|
+
}))
|
|
14318
|
+
};
|
|
14319
|
+
}
|
|
14018
14320
|
function listOutputToWire(out) {
|
|
14019
14321
|
return {
|
|
14020
14322
|
entries: out.entries.map((entry) => ({
|
|
@@ -14045,7 +14347,7 @@ function proFileGetOutputToWire(out) {
|
|
|
14045
14347
|
}
|
|
14046
14348
|
|
|
14047
14349
|
// src/server.ts
|
|
14048
|
-
var SERVER_VERSION = "0.
|
|
14350
|
+
var SERVER_VERSION = "0.4.0";
|
|
14049
14351
|
var toolMeta = (name) => {
|
|
14050
14352
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
14051
14353
|
return { title, description, annotations };
|
|
@@ -14053,6 +14355,7 @@ var toolMeta = (name) => {
|
|
|
14053
14355
|
var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
|
|
14054
14356
|
var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
|
|
14055
14357
|
var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
|
|
14358
|
+
var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
|
|
14056
14359
|
var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
|
|
14057
14360
|
var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
|
|
14058
14361
|
var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
|
|
@@ -14097,7 +14400,8 @@ async function createServer(options) {
|
|
|
14097
14400
|
update,
|
|
14098
14401
|
ephemeral: disk === null,
|
|
14099
14402
|
requests: /* @__PURE__ */ new Map(),
|
|
14100
|
-
payments: /* @__PURE__ */ new Map()
|
|
14403
|
+
payments: /* @__PURE__ */ new Map(),
|
|
14404
|
+
inbox: { items: /* @__PURE__ */ new Map(), version: null }
|
|
14101
14405
|
};
|
|
14102
14406
|
const mcp = new import_mcp.McpServer(
|
|
14103
14407
|
{ name: "sealnet-mcp", version: SERVER_VERSION },
|
|
@@ -14144,6 +14448,7 @@ async function createServer(options) {
|
|
|
14144
14448
|
},
|
|
14145
14449
|
{
|
|
14146
14450
|
publicHost,
|
|
14451
|
+
label: client2.getClientVersion()?.name,
|
|
14147
14452
|
handoff: disableClipboard ? { disableClipboard: true } : void 0,
|
|
14148
14453
|
elicitUrl: urlMode ? async (params) => (await client2.elicitInput({ mode: "url", ...params })).action : void 0,
|
|
14149
14454
|
elicitationDone: urlMode ? (id) => client2.createElicitationCompletionNotifier(id)() : void 0
|
|
@@ -14197,19 +14502,37 @@ async function createServer(options) {
|
|
|
14197
14502
|
{
|
|
14198
14503
|
...toolMeta("seal_open"),
|
|
14199
14504
|
inputSchema: {
|
|
14200
|
-
url
|
|
14505
|
+
// Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
|
|
14506
|
+
url: external_exports.string().url().optional().describe(OPEN_INPUT.url.description),
|
|
14507
|
+
item: external_exports.string().regex(/^in_/).optional().describe(OPEN_INPUT.item.description),
|
|
14201
14508
|
mode: external_exports.enum(["metadata", "inline", "file"]).default("metadata").describe(OPEN_INPUT.mode.description),
|
|
14202
14509
|
dir: external_exports.string().optional().describe(OPEN_INPUT.dir.description)
|
|
14203
14510
|
}
|
|
14204
14511
|
},
|
|
14205
14512
|
async (args, extra) => {
|
|
14206
14513
|
const result = await wrapTool(
|
|
14207
|
-
() => sealOpen(ctx, {
|
|
14514
|
+
() => sealOpen(ctx, {
|
|
14515
|
+
url: args.url,
|
|
14516
|
+
item: args.item,
|
|
14517
|
+
mode: args.mode,
|
|
14518
|
+
dir: args.dir,
|
|
14519
|
+
onProgress: progressReporter(extra)
|
|
14520
|
+
}),
|
|
14208
14521
|
openOutputToWire
|
|
14209
14522
|
);
|
|
14210
14523
|
return result;
|
|
14211
14524
|
}
|
|
14212
14525
|
);
|
|
14526
|
+
mcp.registerTool(
|
|
14527
|
+
"seal_inbox",
|
|
14528
|
+
{
|
|
14529
|
+
...toolMeta("seal_inbox"),
|
|
14530
|
+
inputSchema: {
|
|
14531
|
+
wait: external_exports.number().int().min(0).max(300).default(0).describe(INBOX_INPUT.wait.description)
|
|
14532
|
+
}
|
|
14533
|
+
},
|
|
14534
|
+
async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
|
|
14535
|
+
);
|
|
14213
14536
|
mcp.registerTool(
|
|
14214
14537
|
"seal_list",
|
|
14215
14538
|
{
|