@seekrit/mcp 0.4.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +166 -11
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -664,7 +664,7 @@ function isServiceToken(value) {
664
664
  }
665
665
  //#endregion
666
666
  //#region ../cli/package.json
667
- var version$1 = "0.20.0";
667
+ var version$1 = "0.24.0";
668
668
  const PROJECT_FILE = "seekrit.json";
669
669
  function globalConfigPath() {
670
670
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -726,13 +726,16 @@ var SeekritClient = class {
726
726
  baseUrl;
727
727
  auth;
728
728
  fetchImpl;
729
+ client;
729
730
  constructor(options) {
730
731
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
731
732
  this.auth = options.auth;
732
733
  this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
734
+ this.client = options.client;
733
735
  }
734
736
  async request(method, path, body) {
735
737
  const headers = { accept: "application/json" };
738
+ if (this.client) headers["x-seekrit-client"] = this.client;
736
739
  if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
737
740
  else if (this.auth.type === "dynamic") {
738
741
  const token = await this.auth.getToken();
@@ -883,6 +886,19 @@ var SeekritClient = class {
883
886
  deleteSecret(orgId, envId, name) {
884
887
  return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
885
888
  }
889
+ /** A secret's append-only history, newest version first. */
890
+ listSecretVersions(orgId, envId, name, query = {}) {
891
+ const qs = query.limit === void 0 ? "" : `?limit=${query.limit}`;
892
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/versions${qs}`);
893
+ }
894
+ /**
895
+ * Roll a secret back to an earlier version. Keyless — the server copies the
896
+ * ciphertext it already stores, so this appends a new version rather than
897
+ * rewinding, and needs no DEK on the caller's side.
898
+ */
899
+ restoreSecret(orgId, envId, name, version) {
900
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/restore`, { version });
901
+ }
886
902
  /** The calling principal's wrapped DEK for this environment. */
887
903
  getMyEnvKey(orgId, envId) {
888
904
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
@@ -992,6 +1008,10 @@ var SeekritClient = class {
992
1008
  disableKmsKey(orgId, keyId) {
993
1009
  return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/disable`);
994
1010
  }
1011
+ /** Soft-delete a key: it drops out of every listing and read path. */
1012
+ deleteKmsKey(orgId, keyId) {
1013
+ return this.request("DELETE", `/v1/orgs/${orgId}/kms/keys/${keyId}`);
1014
+ }
995
1015
  /** The broker's public key — wrap the admin credential to it before registering a target. */
996
1016
  getLeaseBrokerKey(orgId) {
997
1017
  return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
@@ -1108,6 +1128,8 @@ function promptHidden(question) {
1108
1128
  }
1109
1129
  //#endregion
1110
1130
  //#region ../cli/src/context.ts
1131
+ /** Sent as `x-seekrit-client` so the API attributes usage to the CLI (analytics). */
1132
+ const CLI_CLIENT = `cli/${version$1}`;
1111
1133
  /**
1112
1134
  * Build the client context from configured credentials, or return null when
1113
1135
  * none are set. `seekrit run` uses this to degrade to a plain launcher instead
@@ -1138,7 +1160,8 @@ function tryBuildContext(dotenvVars = {}) {
1138
1160
  return {
1139
1161
  client: new SeekritClient({
1140
1162
  baseUrl: apiUrl,
1141
- auth
1163
+ auth,
1164
+ client: CLI_CLIENT
1142
1165
  }),
1143
1166
  auth
1144
1167
  };
@@ -1349,7 +1372,8 @@ async function mintAdminToken(apiUrl, creds) {
1349
1372
  type: "m2m",
1350
1373
  clientId: creds.clientId,
1351
1374
  clientSecret: creds.clientSecret
1352
- }
1375
+ },
1376
+ client: `cli/${version$1}`
1353
1377
  });
1354
1378
  const { orgs } = await client.listOrgs();
1355
1379
  const org = orgs[0];
@@ -1418,14 +1442,23 @@ function parseDotenv(content) {
1418
1442
  }
1419
1443
  return out;
1420
1444
  }
1421
- //#endregion
1422
- //#region ../cli/src/secrets.ts
1423
1445
  /** Fetch + decrypt every secret in a single environment. */
1424
1446
  async function fetchDecryptedSecrets(ctx, orgId, envId) {
1425
1447
  const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
1426
1448
  const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
1427
1449
  return Object.fromEntries(entries);
1428
1450
  }
1451
+ /**
1452
+ * Decrypt one historical version of a secret. Ciphertext is bound to
1453
+ * `(envId, name)` as AAD and neither changes across versions, so an old blob
1454
+ * opens with the environment's current data key — no special handling needed.
1455
+ */
1456
+ async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
1457
+ const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
1458
+ const row = versions.find((v) => v.version === version);
1459
+ if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
1460
+ return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
1461
+ }
1429
1462
  async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
1430
1463
  const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
1431
1464
  await ctx.client.setSecret(orgId, envId, name, ciphertext);
@@ -1505,6 +1538,76 @@ function errText(err) {
1505
1538
  isError: true
1506
1539
  };
1507
1540
  }
1541
+ /**
1542
+ * Short primer surfaced as the MCP server `instructions`. Most clients show
1543
+ * this to the model on connect, so it has to orient an agent that lands here
1544
+ * with zero context in a few lines.
1545
+ */
1546
+ function serverInstructions() {
1547
+ return [
1548
+ "seekrit is a zero-knowledge secrets manager. This is the LOCAL CRYPTO plane —",
1549
+ "it runs on this machine, next to your key, so it's the one server that can",
1550
+ "actually read or write a secret VALUE (everything else can only see structure).",
1551
+ "",
1552
+ "Auth comes from SEEKRIT_TOKEN (a skt_… token), SEEKRIT_CLIENT_ID +",
1553
+ "SEEKRIT_CLIENT_SECRET (machine credentials — an admin token is minted and",
1554
+ "cached automatically on first use), or the config saved by `seekrit login`.",
1555
+ "Decrypting under a user session (not a token) additionally needs",
1556
+ "SEEKRIT_PASSPHRASE set in this server's env — there's no TTY to prompt on.",
1557
+ "",
1558
+ "Model: org → apps/groups → environments → secrets. Call `whoami` first, then",
1559
+ "`get_started` for the recommended end-to-end recipe. Prefer `run_command` over",
1560
+ "`get_secret reveal:true` — it injects secrets into a subprocess so plaintext",
1561
+ "never enters your context or the transcript."
1562
+ ].join("\n");
1563
+ }
1564
+ /** The `get_started` tool body: the recommended first-project flow end to end. */
1565
+ function getStartedText() {
1566
+ return [
1567
+ "# Standing up a project with seekrit (agent, end to end)",
1568
+ "",
1569
+ "This server is the crypto plane — every step below runs locally, next to your",
1570
+ "key, so secret values never leave this machine.",
1571
+ "",
1572
+ "## 1. Confirm identity",
1573
+ "`whoami` — see who you're authenticated as and which orgs you can reach. If it",
1574
+ "fails, check SEEKRIT_TOKEN / SEEKRIT_CLIENT_ID+SEEKRIT_CLIENT_SECRET / saved",
1575
+ "login in this server's env.",
1576
+ "",
1577
+ "## 2. Provision structure",
1578
+ "- `create_org` (user session only — a service token can't own one) if you",
1579
+ " don't have one yet, otherwise skip to the next step.",
1580
+ "- `create_app` — an application to hold environments.",
1581
+ "- `create_env` — generates the data key on this machine and grants it to you.",
1582
+ "- `create_group` + `compose_group` (optional) — a reusable secret bag layered",
1583
+ " under one or more app environments.",
1584
+ "",
1585
+ "## 3. Store secrets",
1586
+ "- `set_secret` — encrypts a value locally and stores the ciphertext.",
1587
+ "- `list_secrets` — confirm names + versions (never returns values).",
1588
+ "- `list_secret_versions` + `restore_secret` — undo a bad write by rolling",
1589
+ " back to an earlier version (keyless, and history is append-only).",
1590
+ "",
1591
+ "## 4. Use secrets without exposing them",
1592
+ "- `run_command -- <cmd>` — inject secrets into a subprocess; prefer this.",
1593
+ "- `export_env` — materialize a `.env` file when a tool needs one on disk.",
1594
+ "- `get_secret reveal:true` — only when the value itself is what's needed.",
1595
+ "",
1596
+ "## 5. Propagate access",
1597
+ "- `create_token` bound to an app+env — a runtime credential for CI/another",
1598
+ " agent to decrypt with (pass `admin:true` instead for a provisioning token).",
1599
+ "- `grant_env` — give another member or token access to an env's data key.",
1600
+ "- `configure_project` — link a directory to an org/app (writes seekrit.json)",
1601
+ " so a bound token can infer its target without flags.",
1602
+ "",
1603
+ "## 6. Verify",
1604
+ "`audit` — every write, grant, and revocation is recorded; read it back.",
1605
+ "",
1606
+ "Also available: `kms_*` (client-side managed encrypt/sign keys) and",
1607
+ "`create_pg_lease`/`create_mysql_lease` (short-lived database credentials whose",
1608
+ "password is generated here and never stored anywhere in plaintext)."
1609
+ ].join("\n");
1610
+ }
1508
1611
  /** Build the client context or throw a friendly, agent-readable error. */
1509
1612
  function getCtx() {
1510
1613
  const ctx = tryBuildContext();
@@ -1600,7 +1703,7 @@ async function runMcpServer(options = {}) {
1600
1703
  const server = new McpServer({
1601
1704
  name: "seekrit",
1602
1705
  version: options.version ?? version$1
1603
- });
1706
+ }, { instructions: serverInstructions() });
1604
1707
  /** Register a tool whose handler returns data (serialized) or throws (→ isError). */
1605
1708
  const tool = (name, description, shape, handler) => {
1606
1709
  server.registerTool(name, {
@@ -1614,6 +1717,15 @@ async function runMcpServer(options = {}) {
1614
1717
  }
1615
1718
  }));
1616
1719
  };
1720
+ server.registerTool("get_started", {
1721
+ description: "The recommended first-project recipe: provision structure, store secrets, and use them without exposing values. Call this before doing anything else.",
1722
+ inputSchema: {},
1723
+ annotations: {
1724
+ title: "get_started",
1725
+ readOnlyHint: true,
1726
+ openWorldHint: false
1727
+ }
1728
+ }, async () => jsonText(getStartedText()));
1617
1729
  tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", {}, async () => {
1618
1730
  const ctx = getCtx();
1619
1731
  if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
@@ -1987,10 +2099,11 @@ async function runMcpServer(options = {}) {
1987
2099
  name: o.name
1988
2100
  };
1989
2101
  });
1990
- tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command).", {
2102
+ tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command). Pass `version` to read an earlier version instead of the current one.", {
1991
2103
  ...targetShape,
1992
2104
  name: z.string(),
1993
- reveal: z.boolean().optional()
2105
+ reveal: z.boolean().optional(),
2106
+ version: z.number().int().positive().optional().describe("an earlier version from list_secret_versions (default: current)")
1994
2107
  }, async (o) => {
1995
2108
  const ctx = getCtx();
1996
2109
  const { orgId, envId } = await resolveTargetEnv(ctx, o);
@@ -2000,11 +2113,20 @@ async function runMcpServer(options = {}) {
2000
2113
  if (!row) throw new Error(`no secret named ${o.name}`);
2001
2114
  return {
2002
2115
  name: row.name,
2003
- version: row.version,
2116
+ version: o.version ?? row.version,
2004
2117
  revealed: false
2005
2118
  };
2006
2119
  }
2007
2120
  ensureDecryptable(ctx);
2121
+ if (o.version !== void 0) {
2122
+ const value = await fetchDecryptedVersion(ctx, orgId, envId, o.name, o.version);
2123
+ return {
2124
+ name: o.name,
2125
+ version: o.version,
2126
+ value,
2127
+ revealed: true
2128
+ };
2129
+ }
2008
2130
  const values = await fetchDecryptedSecrets(ctx, orgId, envId);
2009
2131
  if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
2010
2132
  return {
@@ -2013,6 +2135,39 @@ async function runMcpServer(options = {}) {
2013
2135
  revealed: true
2014
2136
  };
2015
2137
  });
2138
+ tool("list_secret_versions", "List a secret's version history: who wrote each version, when, and which ones were restores. Never returns values — pair it with restore_secret to roll back, or get_secret(version, reveal:true) to inspect one.", {
2139
+ ...targetShape,
2140
+ name: z.string(),
2141
+ limit: z.number().int().min(1).max(200).optional().describe("default 20")
2142
+ }, async (o) => {
2143
+ const ctx = getCtx();
2144
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
2145
+ const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, o.name, { limit: o.limit ?? 20 });
2146
+ return {
2147
+ currentVersion,
2148
+ versions: versions.map((v) => ({
2149
+ version: v.version,
2150
+ createdAt: v.createdAt,
2151
+ createdBy: `${v.createdByType}:${v.createdById}`,
2152
+ restoredFromVersion: v.restoredFromVersion
2153
+ }))
2154
+ };
2155
+ });
2156
+ tool("restore_secret", "Roll a secret back to an earlier version. The stored ciphertext is replayed as a NEW version (history is append-only, nothing is overwritten). Keyless — no decryption happens, so this works even without a key.", {
2157
+ ...targetShape,
2158
+ name: z.string(),
2159
+ version: z.number().int().positive()
2160
+ }, async (o) => {
2161
+ const ctx = getCtx();
2162
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
2163
+ const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, o.name, o.version);
2164
+ return {
2165
+ ok: true,
2166
+ name: o.name,
2167
+ restoredFrom,
2168
+ version: secret.version
2169
+ };
2170
+ });
2016
2171
  tool("delete_secret", "Delete a secret from an environment.", {
2017
2172
  ...targetShape,
2018
2173
  name: z.string()
@@ -2301,7 +2456,7 @@ async function runMcpServer(options = {}) {
2301
2456
  /**
2302
2457
  * `@seekrit/mcp` — a standalone, `npx`-able entrypoint for seekrit's MCP server.
2303
2458
  *
2304
- * This is a thin wrapper: the server itself (all 27 tools, the crypto plane of
2459
+ * This is a thin wrapper: the server itself (every tool, the crypto plane of
2305
2460
  * the two-server design) lives in `@seekrit/cli` and is shared with the
2306
2461
  * `seekrit mcp` subcommand — this package just publishes it as its own binary so
2307
2462
  * an agent can run it with zero prior install:
@@ -2314,7 +2469,7 @@ async function runMcpServer(options = {}) {
2314
2469
  * `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
2315
2470
  * published package is self-contained and needs no `@seekrit/cli` install.
2316
2471
  */
2317
- runMcpServer({ version: "0.4.0" }).catch((err) => {
2472
+ runMcpServer({ version: "0.6.0" }).catch((err) => {
2318
2473
  const message = err instanceof Error ? err.message : String(err);
2319
2474
  process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
2320
2475
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@seekrit/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "npx-able MCP server for seekrit — let Claude Code and other MCP clients provision, manage, and inject end-to-end encrypted secrets.",
5
+ "mcpName": "dev.seekrit/mcp",
5
6
  "type": "module",
6
7
  "publishConfig": {
7
8
  "access": "public"
@@ -22,7 +23,7 @@
22
23
  "devDependencies": {
23
24
  "@types/node": "^26.1.0",
24
25
  "tsdown": "^0.22.3",
25
- "@seekrit/cli": "0.20.0"
26
+ "@seekrit/cli": "0.24.0"
26
27
  },
27
28
  "scripts": {
28
29
  "build": "tsdown",