sealnet-mcp 0.4.0 → 0.5.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 CHANGED
@@ -19,6 +19,7 @@ When an MCP-connected AI agent (Claude Desktop, Cursor, Cline, etc.) creates a S
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
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
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 |
22
+ | `seal_run(command, secrets, cwd?, timeout?)` | Run a program with secrets from links or inbox items in its environment, through the `seal` CLI (`seal run --redact`): every value is replaced with `[sealed:NAME]` in the output that comes back, and none enters this server. Needs `seal` on PATH (`run_cli_missing` otherwise). Not a sandbox: a program that prints a value transformed escapes the redaction |
22
23
  | `seal_list()` | List SEALs created by **this MCP server only** (scope: `state.json`; never reveals user's CLI seals) |
23
24
  | `seal_revoke(handle)` | Revoke by opaque handle (model never learns raw `seal_id`) |
24
25
 
package/dist/cli.cjs CHANGED
@@ -11811,7 +11811,7 @@ var seal_mcp_v1_default = {
11811
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`."
11812
11812
  },
11813
11813
  server_instructions: {
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.",
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). Anything sent to you without a link (to your key, your address) waits in seal_inbox: call it, then seal_open item. To run a program with a secret in its environment, use seal_run: the value stays out of the chat. Never read or print a SEAL handoff file.",
11815
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."
11816
11816
  },
11817
11817
  tools: {
@@ -12011,7 +12011,7 @@ var seal_mcp_v1_default = {
12011
12011
  },
12012
12012
  seal_inbox: {
12013
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.",
12014
+ description: "What other agents or people sent to you: seals addressed to your key (your address), no link needed. Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
12015
12015
  annotations: { readOnlyHint: true, openWorldHint: true },
12016
12016
  input: {
12017
12017
  properties: {
@@ -12043,6 +12043,46 @@ var seal_mcp_v1_default = {
12043
12043
  MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
12044
12044
  }
12045
12045
  },
12046
+ seal_run: {
12047
+ title: "Run a program with secrets",
12048
+ description: "Run a program with secrets from SEAL links or your inbox in its environment. The values never enter this conversation: they are removed from the output you get back. Use it instead of reading a secret file.",
12049
+ annotations: { destructiveHint: true, openWorldHint: true },
12050
+ input: {
12051
+ required: ["command", "secrets"],
12052
+ properties: {
12053
+ command: { type: "array", items: { type: "string" }, minItems: 1, description: "The program and its arguments, run without a shell." },
12054
+ secrets: {
12055
+ type: "array",
12056
+ minItems: 1,
12057
+ maxItems: 10,
12058
+ description: "Each one is a link or an inbox item carrying one text file (up to 64 KiB), put into one variable of the program's environment.",
12059
+ items: {
12060
+ one_of: ["url", "item"],
12061
+ properties: {
12062
+ url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
12063
+ item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
12064
+ name: { type: "string", pattern: "^[A-Za-z_][A-Za-z0-9_]*$", description: "The variable; default: the file's name in capitals (`Brave API key` \u2192 `BRAVE_API_KEY`)." }
12065
+ }
12066
+ }
12067
+ },
12068
+ cwd: { type: "string", description: "Working directory of the program; default this server's." },
12069
+ timeout: { type: "integer", minimum: 1, maximum: 600, default: 120, description: "Seconds; after them the program is killed." }
12070
+ }
12071
+ },
12072
+ flow: "SPEC-ADDRESS F4. Runs `seal run --redact --json --seal <url>=<NAME> \u2026 --item <inbox id>=<NAME> \u2026 -- <command\u2026>`, the `seal` CLI found on PATH as seal_request finds it, with this server's key as SEAL_SEED in the CLI's environment only and stdin closed. The CLI opens the links and inbox items with that key, removes SEAL_SEED and SEAL_PASSWORD from the program's environment and replaces every value of 8 bytes or more in its output with `[sealed:NAME]`. The values never enter this server's memory (\u04182). After `timeout` the CLI and the program are killed as one process group. Not a sandbox: the host lets the model run commands anyway, and a program that prints a value transformed (base64, one character at a time) escapes the redaction.",
12073
+ output: {
12074
+ type: "object",
12075
+ required: ["exit_code", "stdout", "stderr", "redactions", "truncated"],
12076
+ properties: {
12077
+ exit_code: { type: "integer", description: "The program's; 128 + the signal when a signal killed it." },
12078
+ stdout: { type: "string", description: "The last 64 KiB of the program's stdout, values replaced." },
12079
+ stderr: { type: "string", description: "The last 64 KiB of its stderr, values replaced." },
12080
+ redactions: { type: "integer", minimum: 0, description: "How many values were replaced." },
12081
+ truncated: { type: "boolean", description: "A stream was longer than 64 KiB: only its end is here." }
12082
+ },
12083
+ MUST_NOT_contain: ["value", "secret_value", "content"]
12084
+ }
12085
+ },
12046
12086
  seal_list: {
12047
12087
  title: "List SEALs created via THIS MCP server",
12048
12088
  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.",
@@ -12390,6 +12430,10 @@ var seal_mcp_v1_default = {
12390
12430
  request_cancelled: "seal_request: the user closed the dialog or declined the link. Ask again only if they want to.",
12391
12431
  request_expired: "seal_request: nobody answered the link before it expired. Make a new request if still needed.",
12392
12432
  request_failed: "seal_request: the local dialog or seal.net failed for a reason other than the above; the message says which.",
12433
+ run_cli_missing: "seal_run: there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh",
12434
+ run_not_text: "seal_run: a link carries more than one file, or a file over 64 KiB or not UTF-8 text: not a secret for an environment.",
12435
+ run_timeout: "seal_run: the program ran longer than `timeout` and was killed with the CLI.",
12436
+ run_failed: "seal_run: the CLI refused before the program started (a bad or gone link, a password, two secrets for one variable, a program that does not start); the message has the CLI's code.",
12393
12437
  pro_secret_not_found: "seal_pro_secret_use: no secret with that id or path in the organisation, or it has no version yet.",
12394
12438
  pro_secret_not_granted: "seal_pro_secret_use: the secret exists but this version is not granted to this agent, or the grant was revoked. A person grants it on the secret's page.",
12395
12439
  pro_secret_expired: "seal_pro_secret_use: the secret version has expired.",
@@ -13474,6 +13518,68 @@ async function sealRevoke(ctx, input) {
13474
13518
  return { revoked: true, revokedAt: now };
13475
13519
  }
13476
13520
 
13521
+ // src/tools/run.ts
13522
+ var import_node_child_process3 = require("child_process");
13523
+ async function sealRun(ctx, input) {
13524
+ const cli = findSealCli();
13525
+ if (!cli) throw new ToolError("run_cli_missing", "there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh");
13526
+ const args = ["--json", "run", "--redact"];
13527
+ for (const secret of input.secrets) {
13528
+ if (secret.url === void 0 === (secret.item === void 0)) {
13529
+ throw new ToolError("run_failed", "each secret takes exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
13530
+ }
13531
+ const name = secret.name === void 0 ? "" : `=${secret.name}`;
13532
+ if (secret.url !== void 0) args.push("--seal", secret.url + name);
13533
+ else args.push("--item", (await inboxItem(ctx, secret.item)).id + name);
13534
+ }
13535
+ args.push("--", ...input.command);
13536
+ const env = { ...process.env, SEAL_SEED: bytesToBase64url(ctx.identity.secret) };
13537
+ const { stdout, timedOut } = await runGroup(cli, args, { cwd: input.cwd, env, timeoutMs: input.timeout * 1e3 });
13538
+ if (timedOut) throw new ToolError("run_timeout", `the program ran longer than ${input.timeout} s and was killed`);
13539
+ let answer;
13540
+ try {
13541
+ answer = JSON.parse(stdout);
13542
+ } catch {
13543
+ throw new ToolError("run_failed", "the `seal` CLI answered no JSON: it is older than 1.4.0 (seal run --redact). Update it: curl -fsSL https://seal.net/install.sh | sh");
13544
+ }
13545
+ if ("error" in answer) {
13546
+ throw new ToolError(answer.error.code === "run.not_text" ? "run_not_text" : "run_failed", `seal run: ${answer.error.code}: ${answer.error.message}`);
13547
+ }
13548
+ return {
13549
+ exitCode: answer.exit_code,
13550
+ stdout: answer.stdout,
13551
+ stderr: answer.stderr,
13552
+ redactions: answer.redactions,
13553
+ truncated: answer.truncated
13554
+ };
13555
+ }
13556
+ function runGroup(cli, args, opts) {
13557
+ const group = process.platform !== "win32";
13558
+ return new Promise((resolve5, reject) => {
13559
+ const child = (0, import_node_child_process3.spawn)(cli, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "ignore"], detached: group });
13560
+ let stdout = "";
13561
+ let timedOut = false;
13562
+ child.stdout.setEncoding("utf8");
13563
+ child.stdout.on("data", (chunk) => stdout += chunk);
13564
+ const timer = setTimeout(() => {
13565
+ timedOut = true;
13566
+ try {
13567
+ if (group && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
13568
+ else child.kill("SIGKILL");
13569
+ } catch {
13570
+ }
13571
+ }, opts.timeoutMs);
13572
+ child.on("error", (err2) => {
13573
+ clearTimeout(timer);
13574
+ reject(new ToolError("run_failed", `could not start ${cli}: ${err2.message}`, err2));
13575
+ });
13576
+ child.on("close", () => {
13577
+ clearTimeout(timer);
13578
+ resolve5({ stdout, timedOut });
13579
+ });
13580
+ });
13581
+ }
13582
+
13477
13583
  // src/tools/share.ts
13478
13584
  var import_node_fs6 = require("fs");
13479
13585
  var import_promises9 = require("fs/promises");
@@ -14317,6 +14423,15 @@ function inboxOutputToWire(out) {
14317
14423
  }))
14318
14424
  };
14319
14425
  }
14426
+ function runOutputToWire(out) {
14427
+ return {
14428
+ exit_code: out.exitCode,
14429
+ stdout: out.stdout,
14430
+ stderr: out.stderr,
14431
+ redactions: out.redactions,
14432
+ truncated: out.truncated
14433
+ };
14434
+ }
14320
14435
  function listOutputToWire(out) {
14321
14436
  return {
14322
14437
  entries: out.entries.map((entry) => ({
@@ -14347,7 +14462,7 @@ function proFileGetOutputToWire(out) {
14347
14462
  }
14348
14463
 
14349
14464
  // src/server.ts
14350
- var SERVER_VERSION = "0.4.0";
14465
+ var SERVER_VERSION = "0.5.0";
14351
14466
  var toolMeta = (name) => {
14352
14467
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
14353
14468
  return { title, description, annotations };
@@ -14356,6 +14471,8 @@ var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
14356
14471
  var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
14357
14472
  var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
14358
14473
  var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
14474
+ var RUN_INPUT = seal_mcp_v1_default.tools.seal_run.input.properties;
14475
+ var RUN_SECRET = RUN_INPUT.secrets.items.properties;
14359
14476
  var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
14360
14477
  var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
14361
14478
  var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
@@ -14533,6 +14650,29 @@ async function createServer(options) {
14533
14650
  },
14534
14651
  async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
14535
14652
  );
14653
+ mcp.registerTool(
14654
+ "seal_run",
14655
+ {
14656
+ ...toolMeta("seal_run"),
14657
+ inputSchema: {
14658
+ command: external_exports.array(external_exports.string()).min(1).describe(RUN_INPUT.command.description),
14659
+ secrets: external_exports.array(
14660
+ external_exports.object({
14661
+ // Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
14662
+ url: external_exports.string().url().optional().describe(RUN_SECRET.url.description),
14663
+ item: external_exports.string().regex(/^in_/).optional().describe(RUN_SECRET.item.description),
14664
+ name: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional().describe(RUN_SECRET.name.description)
14665
+ })
14666
+ ).min(1).max(10).describe(RUN_INPUT.secrets.description),
14667
+ cwd: external_exports.string().optional().describe(RUN_INPUT.cwd.description),
14668
+ timeout: external_exports.number().int().min(1).max(600).default(RUN_INPUT.timeout.default).describe(RUN_INPUT.timeout.description)
14669
+ }
14670
+ },
14671
+ async (args) => wrapTool(
14672
+ () => sealRun(ctx, { command: args.command, secrets: args.secrets, cwd: args.cwd, timeout: args.timeout }),
14673
+ runOutputToWire
14674
+ )
14675
+ );
14536
14676
  mcp.registerTool(
14537
14677
  "seal_list",
14538
14678
  {
package/dist/cli.js CHANGED
@@ -11783,7 +11783,7 @@ var seal_mcp_v1_default = {
11783
11783
  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`."
11784
11784
  },
11785
11785
  server_instructions: {
11786
- 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.",
11786
+ 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). Anything sent to you without a link (to your key, your address) waits in seal_inbox: call it, then seal_open item. To run a program with a secret in its environment, use seal_run: the value stays out of the chat. Never read or print a SEAL handoff file.",
11787
11787
  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."
11788
11788
  },
11789
11789
  tools: {
@@ -11983,7 +11983,7 @@ var seal_mcp_v1_default = {
11983
11983
  },
11984
11984
  seal_inbox: {
11985
11985
  title: "Check the inbox",
11986
- 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.",
11986
+ description: "What other agents or people sent to you: seals addressed to your key (your address), no link needed. Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
11987
11987
  annotations: { readOnlyHint: true, openWorldHint: true },
11988
11988
  input: {
11989
11989
  properties: {
@@ -12015,6 +12015,46 @@ var seal_mcp_v1_default = {
12015
12015
  MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
12016
12016
  }
12017
12017
  },
12018
+ seal_run: {
12019
+ title: "Run a program with secrets",
12020
+ description: "Run a program with secrets from SEAL links or your inbox in its environment. The values never enter this conversation: they are removed from the output you get back. Use it instead of reading a secret file.",
12021
+ annotations: { destructiveHint: true, openWorldHint: true },
12022
+ input: {
12023
+ required: ["command", "secrets"],
12024
+ properties: {
12025
+ command: { type: "array", items: { type: "string" }, minItems: 1, description: "The program and its arguments, run without a shell." },
12026
+ secrets: {
12027
+ type: "array",
12028
+ minItems: 1,
12029
+ maxItems: 10,
12030
+ description: "Each one is a link or an inbox item carrying one text file (up to 64 KiB), put into one variable of the program's environment.",
12031
+ items: {
12032
+ one_of: ["url", "item"],
12033
+ properties: {
12034
+ url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
12035
+ item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
12036
+ name: { type: "string", pattern: "^[A-Za-z_][A-Za-z0-9_]*$", description: "The variable; default: the file's name in capitals (`Brave API key` \u2192 `BRAVE_API_KEY`)." }
12037
+ }
12038
+ }
12039
+ },
12040
+ cwd: { type: "string", description: "Working directory of the program; default this server's." },
12041
+ timeout: { type: "integer", minimum: 1, maximum: 600, default: 120, description: "Seconds; after them the program is killed." }
12042
+ }
12043
+ },
12044
+ flow: "SPEC-ADDRESS F4. Runs `seal run --redact --json --seal <url>=<NAME> \u2026 --item <inbox id>=<NAME> \u2026 -- <command\u2026>`, the `seal` CLI found on PATH as seal_request finds it, with this server's key as SEAL_SEED in the CLI's environment only and stdin closed. The CLI opens the links and inbox items with that key, removes SEAL_SEED and SEAL_PASSWORD from the program's environment and replaces every value of 8 bytes or more in its output with `[sealed:NAME]`. The values never enter this server's memory (\u04182). After `timeout` the CLI and the program are killed as one process group. Not a sandbox: the host lets the model run commands anyway, and a program that prints a value transformed (base64, one character at a time) escapes the redaction.",
12045
+ output: {
12046
+ type: "object",
12047
+ required: ["exit_code", "stdout", "stderr", "redactions", "truncated"],
12048
+ properties: {
12049
+ exit_code: { type: "integer", description: "The program's; 128 + the signal when a signal killed it." },
12050
+ stdout: { type: "string", description: "The last 64 KiB of the program's stdout, values replaced." },
12051
+ stderr: { type: "string", description: "The last 64 KiB of its stderr, values replaced." },
12052
+ redactions: { type: "integer", minimum: 0, description: "How many values were replaced." },
12053
+ truncated: { type: "boolean", description: "A stream was longer than 64 KiB: only its end is here." }
12054
+ },
12055
+ MUST_NOT_contain: ["value", "secret_value", "content"]
12056
+ }
12057
+ },
12018
12058
  seal_list: {
12019
12059
  title: "List SEALs created via THIS MCP server",
12020
12060
  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.",
@@ -12362,6 +12402,10 @@ var seal_mcp_v1_default = {
12362
12402
  request_cancelled: "seal_request: the user closed the dialog or declined the link. Ask again only if they want to.",
12363
12403
  request_expired: "seal_request: nobody answered the link before it expired. Make a new request if still needed.",
12364
12404
  request_failed: "seal_request: the local dialog or seal.net failed for a reason other than the above; the message says which.",
12405
+ run_cli_missing: "seal_run: there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh",
12406
+ run_not_text: "seal_run: a link carries more than one file, or a file over 64 KiB or not UTF-8 text: not a secret for an environment.",
12407
+ run_timeout: "seal_run: the program ran longer than `timeout` and was killed with the CLI.",
12408
+ run_failed: "seal_run: the CLI refused before the program started (a bad or gone link, a password, two secrets for one variable, a program that does not start); the message has the CLI's code.",
12365
12409
  pro_secret_not_found: "seal_pro_secret_use: no secret with that id or path in the organisation, or it has no version yet.",
12366
12410
  pro_secret_not_granted: "seal_pro_secret_use: the secret exists but this version is not granted to this agent, or the grant was revoked. A person grants it on the secret's page.",
12367
12411
  pro_secret_expired: "seal_pro_secret_use: the secret version has expired.",
@@ -13446,6 +13490,68 @@ async function sealRevoke(ctx, input) {
13446
13490
  return { revoked: true, revokedAt: now };
13447
13491
  }
13448
13492
 
13493
+ // src/tools/run.ts
13494
+ import { spawn } from "child_process";
13495
+ async function sealRun(ctx, input) {
13496
+ const cli = findSealCli();
13497
+ if (!cli) throw new ToolError("run_cli_missing", "there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh");
13498
+ const args = ["--json", "run", "--redact"];
13499
+ for (const secret of input.secrets) {
13500
+ if (secret.url === void 0 === (secret.item === void 0)) {
13501
+ throw new ToolError("run_failed", "each secret takes exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
13502
+ }
13503
+ const name = secret.name === void 0 ? "" : `=${secret.name}`;
13504
+ if (secret.url !== void 0) args.push("--seal", secret.url + name);
13505
+ else args.push("--item", (await inboxItem(ctx, secret.item)).id + name);
13506
+ }
13507
+ args.push("--", ...input.command);
13508
+ const env = { ...process.env, SEAL_SEED: bytesToBase64url(ctx.identity.secret) };
13509
+ const { stdout, timedOut } = await runGroup(cli, args, { cwd: input.cwd, env, timeoutMs: input.timeout * 1e3 });
13510
+ if (timedOut) throw new ToolError("run_timeout", `the program ran longer than ${input.timeout} s and was killed`);
13511
+ let answer;
13512
+ try {
13513
+ answer = JSON.parse(stdout);
13514
+ } catch {
13515
+ throw new ToolError("run_failed", "the `seal` CLI answered no JSON: it is older than 1.4.0 (seal run --redact). Update it: curl -fsSL https://seal.net/install.sh | sh");
13516
+ }
13517
+ if ("error" in answer) {
13518
+ throw new ToolError(answer.error.code === "run.not_text" ? "run_not_text" : "run_failed", `seal run: ${answer.error.code}: ${answer.error.message}`);
13519
+ }
13520
+ return {
13521
+ exitCode: answer.exit_code,
13522
+ stdout: answer.stdout,
13523
+ stderr: answer.stderr,
13524
+ redactions: answer.redactions,
13525
+ truncated: answer.truncated
13526
+ };
13527
+ }
13528
+ function runGroup(cli, args, opts) {
13529
+ const group = process.platform !== "win32";
13530
+ return new Promise((resolve5, reject) => {
13531
+ const child = spawn(cli, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "ignore"], detached: group });
13532
+ let stdout = "";
13533
+ let timedOut = false;
13534
+ child.stdout.setEncoding("utf8");
13535
+ child.stdout.on("data", (chunk) => stdout += chunk);
13536
+ const timer = setTimeout(() => {
13537
+ timedOut = true;
13538
+ try {
13539
+ if (group && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
13540
+ else child.kill("SIGKILL");
13541
+ } catch {
13542
+ }
13543
+ }, opts.timeoutMs);
13544
+ child.on("error", (err2) => {
13545
+ clearTimeout(timer);
13546
+ reject(new ToolError("run_failed", `could not start ${cli}: ${err2.message}`, err2));
13547
+ });
13548
+ child.on("close", () => {
13549
+ clearTimeout(timer);
13550
+ resolve5({ stdout, timedOut });
13551
+ });
13552
+ });
13553
+ }
13554
+
13449
13555
  // src/tools/share.ts
13450
13556
  import { openAsBlob as openAsBlob2 } from "fs";
13451
13557
  import { stat as stat3 } from "fs/promises";
@@ -14289,6 +14395,15 @@ function inboxOutputToWire(out) {
14289
14395
  }))
14290
14396
  };
14291
14397
  }
14398
+ function runOutputToWire(out) {
14399
+ return {
14400
+ exit_code: out.exitCode,
14401
+ stdout: out.stdout,
14402
+ stderr: out.stderr,
14403
+ redactions: out.redactions,
14404
+ truncated: out.truncated
14405
+ };
14406
+ }
14292
14407
  function listOutputToWire(out) {
14293
14408
  return {
14294
14409
  entries: out.entries.map((entry) => ({
@@ -14319,7 +14434,7 @@ function proFileGetOutputToWire(out) {
14319
14434
  }
14320
14435
 
14321
14436
  // src/server.ts
14322
- var SERVER_VERSION = "0.4.0";
14437
+ var SERVER_VERSION = "0.5.0";
14323
14438
  var toolMeta = (name) => {
14324
14439
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
14325
14440
  return { title, description, annotations };
@@ -14328,6 +14443,8 @@ var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
14328
14443
  var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
14329
14444
  var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
14330
14445
  var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
14446
+ var RUN_INPUT = seal_mcp_v1_default.tools.seal_run.input.properties;
14447
+ var RUN_SECRET = RUN_INPUT.secrets.items.properties;
14331
14448
  var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
14332
14449
  var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
14333
14450
  var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
@@ -14505,6 +14622,29 @@ async function createServer(options) {
14505
14622
  },
14506
14623
  async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
14507
14624
  );
14625
+ mcp.registerTool(
14626
+ "seal_run",
14627
+ {
14628
+ ...toolMeta("seal_run"),
14629
+ inputSchema: {
14630
+ command: external_exports.array(external_exports.string()).min(1).describe(RUN_INPUT.command.description),
14631
+ secrets: external_exports.array(
14632
+ external_exports.object({
14633
+ // Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
14634
+ url: external_exports.string().url().optional().describe(RUN_SECRET.url.description),
14635
+ item: external_exports.string().regex(/^in_/).optional().describe(RUN_SECRET.item.description),
14636
+ name: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional().describe(RUN_SECRET.name.description)
14637
+ })
14638
+ ).min(1).max(10).describe(RUN_INPUT.secrets.description),
14639
+ cwd: external_exports.string().optional().describe(RUN_INPUT.cwd.description),
14640
+ timeout: external_exports.number().int().min(1).max(600).default(RUN_INPUT.timeout.default).describe(RUN_INPUT.timeout.description)
14641
+ }
14642
+ },
14643
+ async (args) => wrapTool(
14644
+ () => sealRun(ctx, { command: args.command, secrets: args.secrets, cwd: args.cwd, timeout: args.timeout }),
14645
+ runOutputToWire
14646
+ )
14647
+ );
14508
14648
  mcp.registerTool(
14509
14649
  "seal_list",
14510
14650
  {
package/dist/index.cjs CHANGED
@@ -4145,7 +4145,7 @@ var seal_mcp_v1_default = {
4145
4145
  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`."
4146
4146
  },
4147
4147
  server_instructions: {
4148
- 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.",
4148
+ 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). Anything sent to you without a link (to your key, your address) waits in seal_inbox: call it, then seal_open item. To run a program with a secret in its environment, use seal_run: the value stays out of the chat. Never read or print a SEAL handoff file.",
4149
4149
  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."
4150
4150
  },
4151
4151
  tools: {
@@ -4345,7 +4345,7 @@ var seal_mcp_v1_default = {
4345
4345
  },
4346
4346
  seal_inbox: {
4347
4347
  title: "Check the inbox",
4348
- 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.",
4348
+ description: "What other agents or people sent to you: seals addressed to your key (your address), no link needed. Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
4349
4349
  annotations: { readOnlyHint: true, openWorldHint: true },
4350
4350
  input: {
4351
4351
  properties: {
@@ -4377,6 +4377,46 @@ var seal_mcp_v1_default = {
4377
4377
  MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
4378
4378
  }
4379
4379
  },
4380
+ seal_run: {
4381
+ title: "Run a program with secrets",
4382
+ description: "Run a program with secrets from SEAL links or your inbox in its environment. The values never enter this conversation: they are removed from the output you get back. Use it instead of reading a secret file.",
4383
+ annotations: { destructiveHint: true, openWorldHint: true },
4384
+ input: {
4385
+ required: ["command", "secrets"],
4386
+ properties: {
4387
+ command: { type: "array", items: { type: "string" }, minItems: 1, description: "The program and its arguments, run without a shell." },
4388
+ secrets: {
4389
+ type: "array",
4390
+ minItems: 1,
4391
+ maxItems: 10,
4392
+ description: "Each one is a link or an inbox item carrying one text file (up to 64 KiB), put into one variable of the program's environment.",
4393
+ items: {
4394
+ one_of: ["url", "item"],
4395
+ properties: {
4396
+ url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
4397
+ item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
4398
+ name: { type: "string", pattern: "^[A-Za-z_][A-Za-z0-9_]*$", description: "The variable; default: the file's name in capitals (`Brave API key` \u2192 `BRAVE_API_KEY`)." }
4399
+ }
4400
+ }
4401
+ },
4402
+ cwd: { type: "string", description: "Working directory of the program; default this server's." },
4403
+ timeout: { type: "integer", minimum: 1, maximum: 600, default: 120, description: "Seconds; after them the program is killed." }
4404
+ }
4405
+ },
4406
+ flow: "SPEC-ADDRESS F4. Runs `seal run --redact --json --seal <url>=<NAME> \u2026 --item <inbox id>=<NAME> \u2026 -- <command\u2026>`, the `seal` CLI found on PATH as seal_request finds it, with this server's key as SEAL_SEED in the CLI's environment only and stdin closed. The CLI opens the links and inbox items with that key, removes SEAL_SEED and SEAL_PASSWORD from the program's environment and replaces every value of 8 bytes or more in its output with `[sealed:NAME]`. The values never enter this server's memory (\u04182). After `timeout` the CLI and the program are killed as one process group. Not a sandbox: the host lets the model run commands anyway, and a program that prints a value transformed (base64, one character at a time) escapes the redaction.",
4407
+ output: {
4408
+ type: "object",
4409
+ required: ["exit_code", "stdout", "stderr", "redactions", "truncated"],
4410
+ properties: {
4411
+ exit_code: { type: "integer", description: "The program's; 128 + the signal when a signal killed it." },
4412
+ stdout: { type: "string", description: "The last 64 KiB of the program's stdout, values replaced." },
4413
+ stderr: { type: "string", description: "The last 64 KiB of its stderr, values replaced." },
4414
+ redactions: { type: "integer", minimum: 0, description: "How many values were replaced." },
4415
+ truncated: { type: "boolean", description: "A stream was longer than 64 KiB: only its end is here." }
4416
+ },
4417
+ MUST_NOT_contain: ["value", "secret_value", "content"]
4418
+ }
4419
+ },
4380
4420
  seal_list: {
4381
4421
  title: "List SEALs created via THIS MCP server",
4382
4422
  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.",
@@ -4724,6 +4764,10 @@ var seal_mcp_v1_default = {
4724
4764
  request_cancelled: "seal_request: the user closed the dialog or declined the link. Ask again only if they want to.",
4725
4765
  request_expired: "seal_request: nobody answered the link before it expired. Make a new request if still needed.",
4726
4766
  request_failed: "seal_request: the local dialog or seal.net failed for a reason other than the above; the message says which.",
4767
+ run_cli_missing: "seal_run: there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh",
4768
+ run_not_text: "seal_run: a link carries more than one file, or a file over 64 KiB or not UTF-8 text: not a secret for an environment.",
4769
+ run_timeout: "seal_run: the program ran longer than `timeout` and was killed with the CLI.",
4770
+ run_failed: "seal_run: the CLI refused before the program started (a bad or gone link, a password, two secrets for one variable, a program that does not start); the message has the CLI's code.",
4727
4771
  pro_secret_not_found: "seal_pro_secret_use: no secret with that id or path in the organisation, or it has no version yet.",
4728
4772
  pro_secret_not_granted: "seal_pro_secret_use: the secret exists but this version is not granted to this agent, or the grant was revoked. A person grants it on the secret's page.",
4729
4773
  pro_secret_expired: "seal_pro_secret_use: the secret version has expired.",
@@ -13274,6 +13318,68 @@ async function sealRevoke(ctx, input) {
13274
13318
  return { revoked: true, revokedAt: now };
13275
13319
  }
13276
13320
 
13321
+ // src/tools/run.ts
13322
+ var import_node_child_process2 = require("child_process");
13323
+ async function sealRun(ctx, input) {
13324
+ const cli = findSealCli();
13325
+ if (!cli) throw new ToolError("run_cli_missing", "there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh");
13326
+ const args = ["--json", "run", "--redact"];
13327
+ for (const secret of input.secrets) {
13328
+ if (secret.url === void 0 === (secret.item === void 0)) {
13329
+ throw new ToolError("run_failed", "each secret takes exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
13330
+ }
13331
+ const name = secret.name === void 0 ? "" : `=${secret.name}`;
13332
+ if (secret.url !== void 0) args.push("--seal", secret.url + name);
13333
+ else args.push("--item", (await inboxItem(ctx, secret.item)).id + name);
13334
+ }
13335
+ args.push("--", ...input.command);
13336
+ const env = { ...process.env, SEAL_SEED: bytesToBase64url(ctx.identity.secret) };
13337
+ const { stdout, timedOut } = await runGroup(cli, args, { cwd: input.cwd, env, timeoutMs: input.timeout * 1e3 });
13338
+ if (timedOut) throw new ToolError("run_timeout", `the program ran longer than ${input.timeout} s and was killed`);
13339
+ let answer;
13340
+ try {
13341
+ answer = JSON.parse(stdout);
13342
+ } catch {
13343
+ throw new ToolError("run_failed", "the `seal` CLI answered no JSON: it is older than 1.4.0 (seal run --redact). Update it: curl -fsSL https://seal.net/install.sh | sh");
13344
+ }
13345
+ if ("error" in answer) {
13346
+ throw new ToolError(answer.error.code === "run.not_text" ? "run_not_text" : "run_failed", `seal run: ${answer.error.code}: ${answer.error.message}`);
13347
+ }
13348
+ return {
13349
+ exitCode: answer.exit_code,
13350
+ stdout: answer.stdout,
13351
+ stderr: answer.stderr,
13352
+ redactions: answer.redactions,
13353
+ truncated: answer.truncated
13354
+ };
13355
+ }
13356
+ function runGroup(cli, args, opts) {
13357
+ const group = process.platform !== "win32";
13358
+ return new Promise((resolve5, reject) => {
13359
+ const child = (0, import_node_child_process2.spawn)(cli, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "ignore"], detached: group });
13360
+ let stdout = "";
13361
+ let timedOut = false;
13362
+ child.stdout.setEncoding("utf8");
13363
+ child.stdout.on("data", (chunk) => stdout += chunk);
13364
+ const timer = setTimeout(() => {
13365
+ timedOut = true;
13366
+ try {
13367
+ if (group && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
13368
+ else child.kill("SIGKILL");
13369
+ } catch {
13370
+ }
13371
+ }, opts.timeoutMs);
13372
+ child.on("error", (err2) => {
13373
+ clearTimeout(timer);
13374
+ reject(new ToolError("run_failed", `could not start ${cli}: ${err2.message}`, err2));
13375
+ });
13376
+ child.on("close", () => {
13377
+ clearTimeout(timer);
13378
+ resolve5({ stdout, timedOut });
13379
+ });
13380
+ });
13381
+ }
13382
+
13277
13383
  // src/tools/share.ts
13278
13384
  var import_node_fs5 = require("fs");
13279
13385
  var import_promises9 = require("fs/promises");
@@ -14117,6 +14223,15 @@ function inboxOutputToWire(out) {
14117
14223
  }))
14118
14224
  };
14119
14225
  }
14226
+ function runOutputToWire(out) {
14227
+ return {
14228
+ exit_code: out.exitCode,
14229
+ stdout: out.stdout,
14230
+ stderr: out.stderr,
14231
+ redactions: out.redactions,
14232
+ truncated: out.truncated
14233
+ };
14234
+ }
14120
14235
  function listOutputToWire(out) {
14121
14236
  return {
14122
14237
  entries: out.entries.map((entry) => ({
@@ -14147,7 +14262,7 @@ function proFileGetOutputToWire(out) {
14147
14262
  }
14148
14263
 
14149
14264
  // src/server.ts
14150
- var SERVER_VERSION = "0.4.0";
14265
+ var SERVER_VERSION = "0.5.0";
14151
14266
  var toolMeta = (name) => {
14152
14267
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
14153
14268
  return { title, description, annotations };
@@ -14156,6 +14271,8 @@ var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
14156
14271
  var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
14157
14272
  var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
14158
14273
  var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
14274
+ var RUN_INPUT = seal_mcp_v1_default.tools.seal_run.input.properties;
14275
+ var RUN_SECRET = RUN_INPUT.secrets.items.properties;
14159
14276
  var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
14160
14277
  var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
14161
14278
  var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
@@ -14333,6 +14450,29 @@ async function createServer(options) {
14333
14450
  },
14334
14451
  async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
14335
14452
  );
14453
+ mcp.registerTool(
14454
+ "seal_run",
14455
+ {
14456
+ ...toolMeta("seal_run"),
14457
+ inputSchema: {
14458
+ command: external_exports.array(external_exports.string()).min(1).describe(RUN_INPUT.command.description),
14459
+ secrets: external_exports.array(
14460
+ external_exports.object({
14461
+ // Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
14462
+ url: external_exports.string().url().optional().describe(RUN_SECRET.url.description),
14463
+ item: external_exports.string().regex(/^in_/).optional().describe(RUN_SECRET.item.description),
14464
+ name: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional().describe(RUN_SECRET.name.description)
14465
+ })
14466
+ ).min(1).max(10).describe(RUN_INPUT.secrets.description),
14467
+ cwd: external_exports.string().optional().describe(RUN_INPUT.cwd.description),
14468
+ timeout: external_exports.number().int().min(1).max(600).default(RUN_INPUT.timeout.default).describe(RUN_INPUT.timeout.description)
14469
+ }
14470
+ },
14471
+ async (args) => wrapTool(
14472
+ () => sealRun(ctx, { command: args.command, secrets: args.secrets, cwd: args.cwd, timeout: args.timeout }),
14473
+ runOutputToWire
14474
+ )
14475
+ );
14336
14476
  mcp.registerTool(
14337
14477
  "seal_list",
14338
14478
  {
@@ -14499,7 +14639,7 @@ function osDefaultDir(env, platform, home) {
14499
14639
  }
14500
14640
 
14501
14641
  // src/seed.ts
14502
- var import_node_child_process2 = require("child_process");
14642
+ var import_node_child_process3 = require("child_process");
14503
14643
  var import_node_crypto4 = require("crypto");
14504
14644
  var import_node_fs6 = require("fs");
14505
14645
  var import_node_path10 = require("path");
@@ -14533,7 +14673,7 @@ function keyringEntry(keyring, platform, service, account) {
14533
14673
  }
14534
14674
  function readKeytarLinuxEntry(service, account) {
14535
14675
  return new Promise((resolve5) => {
14536
- (0, import_node_child_process2.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
14676
+ (0, import_node_child_process3.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
14537
14677
  const value = err2 ? "" : stdout.replace(/\n$/, "");
14538
14678
  resolve5(value.length > 0 ? value : null);
14539
14679
  });
package/dist/index.d.cts CHANGED
@@ -840,8 +840,8 @@ interface WorkloadCredentials {
840
840
  }
841
841
 
842
842
  /**
843
- * MCP server bootstrap — wires the six tools (`seal_share`,
844
- * `seal_request`, `seal_open`, `seal_inbox`, `seal_list`, `seal_revoke`) to a
843
+ * MCP server bootstrap — wires the seven tools (`seal_share`,
844
+ * `seal_request`, `seal_open`, `seal_inbox`, `seal_run`, `seal_list`, `seal_revoke`) to a
845
845
  * stdio-transport `McpServer` instance, and `seal_pro_secret_use`,
846
846
  * `seal_pro_secret_request` and `seal_pro_file_get` when SEAL Pro workload
847
847
  * credentials exist (contract `pro_tools`).
package/dist/index.d.ts CHANGED
@@ -840,8 +840,8 @@ interface WorkloadCredentials {
840
840
  }
841
841
 
842
842
  /**
843
- * MCP server bootstrap — wires the six tools (`seal_share`,
844
- * `seal_request`, `seal_open`, `seal_inbox`, `seal_list`, `seal_revoke`) to a
843
+ * MCP server bootstrap — wires the seven tools (`seal_share`,
844
+ * `seal_request`, `seal_open`, `seal_inbox`, `seal_run`, `seal_list`, `seal_revoke`) to a
845
845
  * stdio-transport `McpServer` instance, and `seal_pro_secret_use`,
846
846
  * `seal_pro_secret_request` and `seal_pro_file_get` when SEAL Pro workload
847
847
  * credentials exist (contract `pro_tools`).
package/dist/index.js CHANGED
@@ -4065,7 +4065,7 @@ var seal_mcp_v1_default = {
4065
4065
  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`."
4066
4066
  },
4067
4067
  server_instructions: {
4068
- 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.",
4068
+ 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). Anything sent to you without a link (to your key, your address) waits in seal_inbox: call it, then seal_open item. To run a program with a secret in its environment, use seal_run: the value stays out of the chat. Never read or print a SEAL handoff file.",
4069
4069
  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."
4070
4070
  },
4071
4071
  tools: {
@@ -4265,7 +4265,7 @@ var seal_mcp_v1_default = {
4265
4265
  },
4266
4266
  seal_inbox: {
4267
4267
  title: "Check the inbox",
4268
- 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.",
4268
+ description: "What other agents or people sent to you: seals addressed to your key (your address), no link needed. Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
4269
4269
  annotations: { readOnlyHint: true, openWorldHint: true },
4270
4270
  input: {
4271
4271
  properties: {
@@ -4297,6 +4297,46 @@ var seal_mcp_v1_default = {
4297
4297
  MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
4298
4298
  }
4299
4299
  },
4300
+ seal_run: {
4301
+ title: "Run a program with secrets",
4302
+ description: "Run a program with secrets from SEAL links or your inbox in its environment. The values never enter this conversation: they are removed from the output you get back. Use it instead of reading a secret file.",
4303
+ annotations: { destructiveHint: true, openWorldHint: true },
4304
+ input: {
4305
+ required: ["command", "secrets"],
4306
+ properties: {
4307
+ command: { type: "array", items: { type: "string" }, minItems: 1, description: "The program and its arguments, run without a shell." },
4308
+ secrets: {
4309
+ type: "array",
4310
+ minItems: 1,
4311
+ maxItems: 10,
4312
+ description: "Each one is a link or an inbox item carrying one text file (up to 64 KiB), put into one variable of the program's environment.",
4313
+ items: {
4314
+ one_of: ["url", "item"],
4315
+ properties: {
4316
+ url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
4317
+ item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
4318
+ name: { type: "string", pattern: "^[A-Za-z_][A-Za-z0-9_]*$", description: "The variable; default: the file's name in capitals (`Brave API key` \u2192 `BRAVE_API_KEY`)." }
4319
+ }
4320
+ }
4321
+ },
4322
+ cwd: { type: "string", description: "Working directory of the program; default this server's." },
4323
+ timeout: { type: "integer", minimum: 1, maximum: 600, default: 120, description: "Seconds; after them the program is killed." }
4324
+ }
4325
+ },
4326
+ flow: "SPEC-ADDRESS F4. Runs `seal run --redact --json --seal <url>=<NAME> \u2026 --item <inbox id>=<NAME> \u2026 -- <command\u2026>`, the `seal` CLI found on PATH as seal_request finds it, with this server's key as SEAL_SEED in the CLI's environment only and stdin closed. The CLI opens the links and inbox items with that key, removes SEAL_SEED and SEAL_PASSWORD from the program's environment and replaces every value of 8 bytes or more in its output with `[sealed:NAME]`. The values never enter this server's memory (\u04182). After `timeout` the CLI and the program are killed as one process group. Not a sandbox: the host lets the model run commands anyway, and a program that prints a value transformed (base64, one character at a time) escapes the redaction.",
4327
+ output: {
4328
+ type: "object",
4329
+ required: ["exit_code", "stdout", "stderr", "redactions", "truncated"],
4330
+ properties: {
4331
+ exit_code: { type: "integer", description: "The program's; 128 + the signal when a signal killed it." },
4332
+ stdout: { type: "string", description: "The last 64 KiB of the program's stdout, values replaced." },
4333
+ stderr: { type: "string", description: "The last 64 KiB of its stderr, values replaced." },
4334
+ redactions: { type: "integer", minimum: 0, description: "How many values were replaced." },
4335
+ truncated: { type: "boolean", description: "A stream was longer than 64 KiB: only its end is here." }
4336
+ },
4337
+ MUST_NOT_contain: ["value", "secret_value", "content"]
4338
+ }
4339
+ },
4300
4340
  seal_list: {
4301
4341
  title: "List SEALs created via THIS MCP server",
4302
4342
  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.",
@@ -4644,6 +4684,10 @@ var seal_mcp_v1_default = {
4644
4684
  request_cancelled: "seal_request: the user closed the dialog or declined the link. Ask again only if they want to.",
4645
4685
  request_expired: "seal_request: nobody answered the link before it expired. Make a new request if still needed.",
4646
4686
  request_failed: "seal_request: the local dialog or seal.net failed for a reason other than the above; the message says which.",
4687
+ run_cli_missing: "seal_run: there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh",
4688
+ run_not_text: "seal_run: a link carries more than one file, or a file over 64 KiB or not UTF-8 text: not a secret for an environment.",
4689
+ run_timeout: "seal_run: the program ran longer than `timeout` and was killed with the CLI.",
4690
+ run_failed: "seal_run: the CLI refused before the program started (a bad or gone link, a password, two secrets for one variable, a program that does not start); the message has the CLI's code.",
4647
4691
  pro_secret_not_found: "seal_pro_secret_use: no secret with that id or path in the organisation, or it has no version yet.",
4648
4692
  pro_secret_not_granted: "seal_pro_secret_use: the secret exists but this version is not granted to this agent, or the grant was revoked. A person grants it on the secret's page.",
4649
4693
  pro_secret_expired: "seal_pro_secret_use: the secret version has expired.",
@@ -13194,6 +13238,68 @@ async function sealRevoke(ctx, input) {
13194
13238
  return { revoked: true, revokedAt: now };
13195
13239
  }
13196
13240
 
13241
+ // src/tools/run.ts
13242
+ import { spawn } from "child_process";
13243
+ async function sealRun(ctx, input) {
13244
+ const cli = findSealCli();
13245
+ if (!cli) throw new ToolError("run_cli_missing", "there is no `seal` CLI on PATH. Install it: curl -fsSL https://seal.net/install.sh | sh");
13246
+ const args = ["--json", "run", "--redact"];
13247
+ for (const secret of input.secrets) {
13248
+ if (secret.url === void 0 === (secret.item === void 0)) {
13249
+ throw new ToolError("run_failed", "each secret takes exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
13250
+ }
13251
+ const name = secret.name === void 0 ? "" : `=${secret.name}`;
13252
+ if (secret.url !== void 0) args.push("--seal", secret.url + name);
13253
+ else args.push("--item", (await inboxItem(ctx, secret.item)).id + name);
13254
+ }
13255
+ args.push("--", ...input.command);
13256
+ const env = { ...process.env, SEAL_SEED: bytesToBase64url(ctx.identity.secret) };
13257
+ const { stdout, timedOut } = await runGroup(cli, args, { cwd: input.cwd, env, timeoutMs: input.timeout * 1e3 });
13258
+ if (timedOut) throw new ToolError("run_timeout", `the program ran longer than ${input.timeout} s and was killed`);
13259
+ let answer;
13260
+ try {
13261
+ answer = JSON.parse(stdout);
13262
+ } catch {
13263
+ throw new ToolError("run_failed", "the `seal` CLI answered no JSON: it is older than 1.4.0 (seal run --redact). Update it: curl -fsSL https://seal.net/install.sh | sh");
13264
+ }
13265
+ if ("error" in answer) {
13266
+ throw new ToolError(answer.error.code === "run.not_text" ? "run_not_text" : "run_failed", `seal run: ${answer.error.code}: ${answer.error.message}`);
13267
+ }
13268
+ return {
13269
+ exitCode: answer.exit_code,
13270
+ stdout: answer.stdout,
13271
+ stderr: answer.stderr,
13272
+ redactions: answer.redactions,
13273
+ truncated: answer.truncated
13274
+ };
13275
+ }
13276
+ function runGroup(cli, args, opts) {
13277
+ const group = process.platform !== "win32";
13278
+ return new Promise((resolve5, reject) => {
13279
+ const child = spawn(cli, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "ignore"], detached: group });
13280
+ let stdout = "";
13281
+ let timedOut = false;
13282
+ child.stdout.setEncoding("utf8");
13283
+ child.stdout.on("data", (chunk) => stdout += chunk);
13284
+ const timer = setTimeout(() => {
13285
+ timedOut = true;
13286
+ try {
13287
+ if (group && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
13288
+ else child.kill("SIGKILL");
13289
+ } catch {
13290
+ }
13291
+ }, opts.timeoutMs);
13292
+ child.on("error", (err2) => {
13293
+ clearTimeout(timer);
13294
+ reject(new ToolError("run_failed", `could not start ${cli}: ${err2.message}`, err2));
13295
+ });
13296
+ child.on("close", () => {
13297
+ clearTimeout(timer);
13298
+ resolve5({ stdout, timedOut });
13299
+ });
13300
+ });
13301
+ }
13302
+
13197
13303
  // src/tools/share.ts
13198
13304
  import { openAsBlob as openAsBlob2 } from "fs";
13199
13305
  import { stat as stat3 } from "fs/promises";
@@ -14037,6 +14143,15 @@ function inboxOutputToWire(out) {
14037
14143
  }))
14038
14144
  };
14039
14145
  }
14146
+ function runOutputToWire(out) {
14147
+ return {
14148
+ exit_code: out.exitCode,
14149
+ stdout: out.stdout,
14150
+ stderr: out.stderr,
14151
+ redactions: out.redactions,
14152
+ truncated: out.truncated
14153
+ };
14154
+ }
14040
14155
  function listOutputToWire(out) {
14041
14156
  return {
14042
14157
  entries: out.entries.map((entry) => ({
@@ -14067,7 +14182,7 @@ function proFileGetOutputToWire(out) {
14067
14182
  }
14068
14183
 
14069
14184
  // src/server.ts
14070
- var SERVER_VERSION = "0.4.0";
14185
+ var SERVER_VERSION = "0.5.0";
14071
14186
  var toolMeta = (name) => {
14072
14187
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
14073
14188
  return { title, description, annotations };
@@ -14076,6 +14191,8 @@ var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
14076
14191
  var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
14077
14192
  var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
14078
14193
  var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
14194
+ var RUN_INPUT = seal_mcp_v1_default.tools.seal_run.input.properties;
14195
+ var RUN_SECRET = RUN_INPUT.secrets.items.properties;
14079
14196
  var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
14080
14197
  var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
14081
14198
  var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
@@ -14253,6 +14370,29 @@ async function createServer(options) {
14253
14370
  },
14254
14371
  async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
14255
14372
  );
14373
+ mcp.registerTool(
14374
+ "seal_run",
14375
+ {
14376
+ ...toolMeta("seal_run"),
14377
+ inputSchema: {
14378
+ command: external_exports.array(external_exports.string()).min(1).describe(RUN_INPUT.command.description),
14379
+ secrets: external_exports.array(
14380
+ external_exports.object({
14381
+ // Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
14382
+ url: external_exports.string().url().optional().describe(RUN_SECRET.url.description),
14383
+ item: external_exports.string().regex(/^in_/).optional().describe(RUN_SECRET.item.description),
14384
+ name: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional().describe(RUN_SECRET.name.description)
14385
+ })
14386
+ ).min(1).max(10).describe(RUN_INPUT.secrets.description),
14387
+ cwd: external_exports.string().optional().describe(RUN_INPUT.cwd.description),
14388
+ timeout: external_exports.number().int().min(1).max(600).default(RUN_INPUT.timeout.default).describe(RUN_INPUT.timeout.description)
14389
+ }
14390
+ },
14391
+ async (args) => wrapTool(
14392
+ () => sealRun(ctx, { command: args.command, secrets: args.secrets, cwd: args.cwd, timeout: args.timeout }),
14393
+ runOutputToWire
14394
+ )
14395
+ );
14256
14396
  mcp.registerTool(
14257
14397
  "seal_list",
14258
14398
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sealnet-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "mcpName": "net.seal/mcp",
5
5
  "description": "Model Context Protocol server for SEAL: handoff mode puts the link on the user's clipboard or in a 0600 file, never in the chat; explicit opt-in to forward mode with strict TTL/recipient policy.",
6
6
  "type": "module",