@seekrit/mcp 0.3.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.
Files changed (3) hide show
  1. package/README.md +31 -6
  2. package/dist/index.js +188 -7
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -18,15 +18,38 @@ it just publishes it as its own binary so no CLI install is required first.
18
18
  ## Setup
19
19
 
20
20
  Add it to any MCP client with a stdio server whose command runs this package via
21
- `npx`, passing a credential in its environment. For Claude Code:
21
+ `npx`, passing a credential in its environment.
22
+
23
+ **Fully autonomous agent (recommended).** An agent with no account can
24
+ [sign up](https://seekrit.dev/docs/guides/ai-agents) for **machine credentials**
25
+ (`POST https://mcp.seekrit.dev/signup`) and use the *same* client id + secret for
26
+ everything — the hosted metadata plane and this local crypto plane. Point this
27
+ server at them and it mints its own admin token automatically on first use
28
+ (keyless — the keypair is generated locally), so there is no token to copy:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "seekrit": {
34
+ "command": "npx",
35
+ "args": ["-y", "@seekrit/mcp"],
36
+ "env": {
37
+ "SEEKRIT_CLIENT_ID": "<your client id>",
38
+ "SEEKRIT_CLIENT_SECRET": "<your client secret>"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ **With an existing token.** If you already hold a service token, pass it directly.
46
+ For Claude Code:
22
47
 
23
48
  ```sh
24
49
  # An admin token lets the agent provision structure (apps/envs/tokens) too:
25
50
  claude mcp add seekrit --env SEEKRIT_TOKEN=skt_… -- npx -y @seekrit/mcp
26
51
  ```
27
52
 
28
- Or drop a `.mcp.json` in your project — the whole setup is this snippet:
29
-
30
53
  ```json
31
54
  {
32
55
  "mcpServers": {
@@ -73,12 +96,14 @@ The container still decrypts locally — nothing plaintext leaves it.
73
96
 
74
97
  ## Choosing the credential
75
98
 
76
- The server authenticates exactly like the CLI — `SEEKRIT_TOKEN`, or saved
77
- config at `~/.config/seekrit/config.json`.
99
+ The server authenticates exactly like the CLI — `SEEKRIT_CLIENT_ID` +
100
+ `SEEKRIT_CLIENT_SECRET`, `SEEKRIT_TOKEN`, or saved config at
101
+ `~/.config/seekrit/config.json`.
78
102
 
79
103
  | Credential | Good for | Notes |
80
104
  | --- | --- | --- |
81
- | **Admin token** (`seekrit token create --admin`) | Provisioning: create apps/groups/envs, compose, grant, mint tokens | Org-scoped; the only headless way to create structure. |
105
+ | **Machine credentials** (`SEEKRIT_CLIENT_ID` + `SEEKRIT_CLIENT_SECRET`) | Fully autonomous agents: one credential for both planes | Auto-mints + caches an admin token on first use. Get them from `POST /signup`. |
106
+ | **Admin token** (`seekrit token create --admin`) | Provisioning: create apps/groups/envs, compose, grant, mint tokens | Org-scoped; a fixed headless credential for structure. |
82
107
  | **Runtime token** (bound to an env) | Reading/writing/injecting one environment's secrets | Self-decrypts — no passphrase. Cannot provision. |
83
108
 
84
109
  Under user auth (no token), tools that decrypt need `SEEKRIT_PASSPHRASE` in the
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { z } from "zod";
6
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { dirname, join, parse } from "node:path";
9
9
  import { createInterface } from "node:readline";
@@ -664,7 +664,7 @@ function isServiceToken(value) {
664
664
  }
665
665
  //#endregion
666
666
  //#region ../cli/package.json
667
- var version$1 = "0.19.0";
667
+ var version$1 = "0.23.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");
@@ -674,6 +674,26 @@ function readGlobalConfig() {
674
674
  if (!existsSync(path)) return {};
675
675
  return JSON.parse(readFileSync(path, "utf8"));
676
676
  }
677
+ function writeGlobalConfig(update) {
678
+ const path = globalConfigPath();
679
+ const merged = {
680
+ ...readGlobalConfig(),
681
+ ...update
682
+ };
683
+ mkdirSync(dirname(path), { recursive: true });
684
+ writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
685
+ }
686
+ /** The admin token previously auto-minted from these M2M credentials, if any. */
687
+ function readCachedM2mToken(clientId) {
688
+ return readGlobalConfig().m2mTokens?.[clientId];
689
+ }
690
+ /** Remember an auto-minted admin token so the next run reuses it. */
691
+ function cacheM2mToken(clientId, token) {
692
+ writeGlobalConfig({ m2mTokens: {
693
+ ...readGlobalConfig().m2mTokens,
694
+ [clientId]: token
695
+ } });
696
+ }
677
697
  /** Walk up from cwd looking for seekrit.json. */
678
698
  function findProjectConfig(startDir = process.cwd()) {
679
699
  let dir = startDir;
@@ -706,19 +726,23 @@ var SeekritClient = class {
706
726
  baseUrl;
707
727
  auth;
708
728
  fetchImpl;
729
+ client;
709
730
  constructor(options) {
710
731
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
711
732
  this.auth = options.auth;
712
733
  this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
734
+ this.client = options.client;
713
735
  }
714
736
  async request(method, path, body) {
715
737
  const headers = { accept: "application/json" };
738
+ if (this.client) headers["x-seekrit-client"] = this.client;
716
739
  if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
717
740
  else if (this.auth.type === "dynamic") {
718
741
  const token = await this.auth.getToken();
719
742
  if (!token) throw new SeekritApiError(401, "unauthorized", "session expired");
720
743
  headers.authorization = `Bearer ${token}`;
721
- } else headers["x-seekrit-dev-user"] = this.auth.email;
744
+ } else if (this.auth.type === "m2m") headers.authorization = `Basic ${btoa(`${this.auth.clientId}:${this.auth.clientSecret}`)}`;
745
+ else headers["x-seekrit-dev-user"] = this.auth.email;
722
746
  if (body !== void 0) headers["content-type"] = "application/json";
723
747
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
724
748
  method,
@@ -971,6 +995,10 @@ var SeekritClient = class {
971
995
  disableKmsKey(orgId, keyId) {
972
996
  return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/disable`);
973
997
  }
998
+ /** Soft-delete a key: it drops out of every listing and read path. */
999
+ deleteKmsKey(orgId, keyId) {
1000
+ return this.request("DELETE", `/v1/orgs/${orgId}/kms/keys/${keyId}`);
1001
+ }
974
1002
  /** The broker's public key — wrap the admin credential to it before registering a target. */
975
1003
  getLeaseBrokerKey(orgId) {
976
1004
  return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
@@ -1087,6 +1115,8 @@ function promptHidden(question) {
1087
1115
  }
1088
1116
  //#endregion
1089
1117
  //#region ../cli/src/context.ts
1118
+ /** Sent as `x-seekrit-client` so the API attributes usage to the CLI (analytics). */
1119
+ const CLI_CLIENT = `cli/${version$1}`;
1090
1120
  /**
1091
1121
  * Build the client context from configured credentials, or return null when
1092
1122
  * none are set. `seekrit run` uses this to degrade to a plain launcher instead
@@ -1117,7 +1147,8 @@ function tryBuildContext(dotenvVars = {}) {
1117
1147
  return {
1118
1148
  client: new SeekritClient({
1119
1149
  baseUrl: apiUrl,
1120
- auth
1150
+ auth,
1151
+ client: CLI_CLIENT
1121
1152
  }),
1122
1153
  auth
1123
1154
  };
@@ -1296,6 +1327,78 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
1296
1327
  };
1297
1328
  }
1298
1329
  //#endregion
1330
+ //#region ../cli/src/m2m.ts
1331
+ /**
1332
+ * Resolve M2M client credentials from (in order) the process environment, a
1333
+ * `.env` overlay (for `seekrit run`), then saved config. Both halves must come
1334
+ * through for the credential to be usable.
1335
+ */
1336
+ function readM2mCreds(dotenvVars = {}) {
1337
+ const config = readGlobalConfig();
1338
+ const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
1339
+ const clientId = fromEnv("SEEKRIT_CLIENT_ID") ?? config.clientId;
1340
+ const clientSecret = fromEnv("SEEKRIT_CLIENT_SECRET") ?? config.clientSecret;
1341
+ if (!clientId || !clientSecret) return null;
1342
+ return {
1343
+ clientId,
1344
+ clientSecret
1345
+ };
1346
+ }
1347
+ /** True when a service/dev credential is already configured explicitly. */
1348
+ function hasExplicitCredential(dotenvVars) {
1349
+ const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
1350
+ if (fromEnv("SEEKRIT_TOKEN") || fromEnv("SEEKRIT_DEV_USER")) return true;
1351
+ const config = readGlobalConfig();
1352
+ return Boolean(config.token || config.devUser);
1353
+ }
1354
+ /** Mint a fresh org-scoped admin token using M2M credentials (keyless). */
1355
+ async function mintAdminToken(apiUrl, creds) {
1356
+ const client = new SeekritClient({
1357
+ baseUrl: apiUrl,
1358
+ auth: {
1359
+ type: "m2m",
1360
+ clientId: creds.clientId,
1361
+ clientSecret: creds.clientSecret
1362
+ },
1363
+ client: `cli/${version$1}`
1364
+ });
1365
+ const { orgs } = await client.listOrgs();
1366
+ const org = orgs[0];
1367
+ if (!org) throw new Error("machine client has no organization — sign up first (POST /signup)");
1368
+ const created = await createServiceToken();
1369
+ await client.createToken(org.id, {
1370
+ name: "agent-admin (auto)",
1371
+ tokenId: created.tokenId,
1372
+ tokenHash: created.tokenHash,
1373
+ publicKeyJwk: created.publicKeyJwk,
1374
+ role: "admin",
1375
+ environmentId: null
1376
+ });
1377
+ return created.token;
1378
+ }
1379
+ /**
1380
+ * If M2M credentials are configured (and no explicit token/dev-user is), ensure
1381
+ * an admin token exists for them — minting and caching one on first use — and
1382
+ * expose it as `SEEKRIT_TOKEN` so the normal credential resolution
1383
+ * ({@link tryBuildContext}) picks it up. A no-op (returns null) when there is
1384
+ * nothing to bootstrap. Throws only when credentials ARE present but minting
1385
+ * fails, so the reason surfaces instead of a later opaque "no credentials".
1386
+ */
1387
+ async function ensureM2mAdminToken(dotenvVars = {}) {
1388
+ if (hasExplicitCredential(dotenvVars)) return null;
1389
+ const creds = readM2mCreds(dotenvVars);
1390
+ if (!creds) return null;
1391
+ const cached = readCachedM2mToken(creds.clientId);
1392
+ if (cached) {
1393
+ process.env.SEEKRIT_TOKEN = cached;
1394
+ return cached;
1395
+ }
1396
+ const token = await mintAdminToken(process.env.SEEKRIT_API_URL ?? dotenvVars.SEEKRIT_API_URL ?? readGlobalConfig().apiUrl ?? "https://api.seekrit.dev", creds);
1397
+ cacheM2mToken(creds.clientId, token);
1398
+ process.env.SEEKRIT_TOKEN = token;
1399
+ return token;
1400
+ }
1401
+ //#endregion
1299
1402
  //#region ../cli/src/dotenv.ts
1300
1403
  /**
1301
1404
  * Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
@@ -1413,10 +1516,78 @@ function errText(err) {
1413
1516
  isError: true
1414
1517
  };
1415
1518
  }
1519
+ /**
1520
+ * Short primer surfaced as the MCP server `instructions`. Most clients show
1521
+ * this to the model on connect, so it has to orient an agent that lands here
1522
+ * with zero context in a few lines.
1523
+ */
1524
+ function serverInstructions() {
1525
+ return [
1526
+ "seekrit is a zero-knowledge secrets manager. This is the LOCAL CRYPTO plane —",
1527
+ "it runs on this machine, next to your key, so it's the one server that can",
1528
+ "actually read or write a secret VALUE (everything else can only see structure).",
1529
+ "",
1530
+ "Auth comes from SEEKRIT_TOKEN (a skt_… token), SEEKRIT_CLIENT_ID +",
1531
+ "SEEKRIT_CLIENT_SECRET (machine credentials — an admin token is minted and",
1532
+ "cached automatically on first use), or the config saved by `seekrit login`.",
1533
+ "Decrypting under a user session (not a token) additionally needs",
1534
+ "SEEKRIT_PASSPHRASE set in this server's env — there's no TTY to prompt on.",
1535
+ "",
1536
+ "Model: org → apps/groups → environments → secrets. Call `whoami` first, then",
1537
+ "`get_started` for the recommended end-to-end recipe. Prefer `run_command` over",
1538
+ "`get_secret reveal:true` — it injects secrets into a subprocess so plaintext",
1539
+ "never enters your context or the transcript."
1540
+ ].join("\n");
1541
+ }
1542
+ /** The `get_started` tool body: the recommended first-project flow end to end. */
1543
+ function getStartedText() {
1544
+ return [
1545
+ "# Standing up a project with seekrit (agent, end to end)",
1546
+ "",
1547
+ "This server is the crypto plane — every step below runs locally, next to your",
1548
+ "key, so secret values never leave this machine.",
1549
+ "",
1550
+ "## 1. Confirm identity",
1551
+ "`whoami` — see who you're authenticated as and which orgs you can reach. If it",
1552
+ "fails, check SEEKRIT_TOKEN / SEEKRIT_CLIENT_ID+SEEKRIT_CLIENT_SECRET / saved",
1553
+ "login in this server's env.",
1554
+ "",
1555
+ "## 2. Provision structure",
1556
+ "- `create_org` (user session only — a service token can't own one) if you",
1557
+ " don't have one yet, otherwise skip to the next step.",
1558
+ "- `create_app` — an application to hold environments.",
1559
+ "- `create_env` — generates the data key on this machine and grants it to you.",
1560
+ "- `create_group` + `compose_group` (optional) — a reusable secret bag layered",
1561
+ " under one or more app environments.",
1562
+ "",
1563
+ "## 3. Store secrets",
1564
+ "- `set_secret` — encrypts a value locally and stores the ciphertext.",
1565
+ "- `list_secrets` — confirm names + versions (never returns values).",
1566
+ "",
1567
+ "## 4. Use secrets without exposing them",
1568
+ "- `run_command -- <cmd>` — inject secrets into a subprocess; prefer this.",
1569
+ "- `export_env` — materialize a `.env` file when a tool needs one on disk.",
1570
+ "- `get_secret reveal:true` — only when the value itself is what's needed.",
1571
+ "",
1572
+ "## 5. Propagate access",
1573
+ "- `create_token` bound to an app+env — a runtime credential for CI/another",
1574
+ " agent to decrypt with (pass `admin:true` instead for a provisioning token).",
1575
+ "- `grant_env` — give another member or token access to an env's data key.",
1576
+ "- `configure_project` — link a directory to an org/app (writes seekrit.json)",
1577
+ " so a bound token can infer its target without flags.",
1578
+ "",
1579
+ "## 6. Verify",
1580
+ "`audit` — every write, grant, and revocation is recorded; read it back.",
1581
+ "",
1582
+ "Also available: `kms_*` (client-side managed encrypt/sign keys) and",
1583
+ "`create_pg_lease`/`create_mysql_lease` (short-lived database credentials whose",
1584
+ "password is generated here and never stored anywhere in plaintext)."
1585
+ ].join("\n");
1586
+ }
1416
1587
  /** Build the client context or throw a friendly, agent-readable error. */
1417
1588
  function getCtx() {
1418
1589
  const ctx = tryBuildContext();
1419
- if (!ctx) throw new Error("no credentials — set SEEKRIT_TOKEN (a skt_… token) or SEEKRIT_DEV_USER in the MCP server env, or run `seekrit login` first");
1590
+ if (!ctx) throw new Error("no credentials — set SEEKRIT_CLIENT_ID + SEEKRIT_CLIENT_SECRET (machine credentials from POST /signup) or SEEKRIT_TOKEN (a skt_… token) in the MCP server env, or run `seekrit login` first");
1420
1591
  return ctx;
1421
1592
  }
1422
1593
  /**
@@ -1504,10 +1675,11 @@ async function resolveTargetEnv(ctx, o) {
1504
1675
  }
1505
1676
  async function runMcpServer(options = {}) {
1506
1677
  setFailThrows(true);
1678
+ await ensureM2mAdminToken();
1507
1679
  const server = new McpServer({
1508
1680
  name: "seekrit",
1509
1681
  version: options.version ?? version$1
1510
- });
1682
+ }, { instructions: serverInstructions() });
1511
1683
  /** Register a tool whose handler returns data (serialized) or throws (→ isError). */
1512
1684
  const tool = (name, description, shape, handler) => {
1513
1685
  server.registerTool(name, {
@@ -1521,6 +1693,15 @@ async function runMcpServer(options = {}) {
1521
1693
  }
1522
1694
  }));
1523
1695
  };
1696
+ server.registerTool("get_started", {
1697
+ description: "The recommended first-project recipe: provision structure, store secrets, and use them without exposing values. Call this before doing anything else.",
1698
+ inputSchema: {},
1699
+ annotations: {
1700
+ title: "get_started",
1701
+ readOnlyHint: true,
1702
+ openWorldHint: false
1703
+ }
1704
+ }, async () => jsonText(getStartedText()));
1524
1705
  tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", {}, async () => {
1525
1706
  const ctx = getCtx();
1526
1707
  if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
@@ -2221,7 +2402,7 @@ async function runMcpServer(options = {}) {
2221
2402
  * `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
2222
2403
  * published package is self-contained and needs no `@seekrit/cli` install.
2223
2404
  */
2224
- runMcpServer({ version: "0.3.0" }).catch((err) => {
2405
+ runMcpServer({ version: "0.5.0" }).catch((err) => {
2225
2406
  const message = err instanceof Error ? err.message : String(err);
2226
2407
  process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
2227
2408
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@seekrit/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.19.0"
26
+ "@seekrit/cli": "0.23.0"
26
27
  },
27
28
  "scripts": {
28
29
  "build": "tsdown",