@seekrit/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -315,7 +315,7 @@ async function unwrapDek(wrapped, privateKey) {
315
315
  }
316
316
  //#endregion
317
317
  //#region package.json
318
- var version = "0.2.0";
318
+ var version = "0.4.0";
319
319
  const PROJECT_FILE = "seekrit.json";
320
320
  function globalConfigPath() {
321
321
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -518,7 +518,17 @@ var SeekritClient = class {
518
518
  };
519
519
  //#endregion
520
520
  //#region src/io.ts
521
+ let failThrows = false;
522
+ /**
523
+ * In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
524
+ * must surface as a catchable error (→ a tool error result) rather than
525
+ * exiting and killing every other tool. Toggled on once at MCP startup.
526
+ */
527
+ function setFailThrows(value) {
528
+ failThrows = value;
529
+ }
521
530
  function fail(message) {
531
+ if (failThrows) throw new Error(message);
522
532
  console.error(`error: ${message}`);
523
533
  process.exit(1);
524
534
  }
@@ -553,12 +563,19 @@ async function readStdin() {
553
563
  * Build the client context from configured credentials, or return null when
554
564
  * none are set. `seekrit run` uses this to degrade to a plain launcher instead
555
565
  * of exiting; every other command goes through `buildContext`, which fails.
566
+ *
567
+ * `dotenvVars` supplies `SEEKRIT_*` values read from a `.env` file. They sit
568
+ * below the live `process.env` but above the saved config, so a project-local
569
+ * `.env` can carry the token / API URL — matching `seekrit-run`'s
570
+ * `flag > env > .env` credential resolution. Empty for every command but
571
+ * `seekrit run`, which loads `.env` before authenticating.
556
572
  */
557
- function tryBuildContext() {
573
+ function tryBuildContext(dotenvVars = {}) {
558
574
  const config = readGlobalConfig();
559
- const apiUrl = process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "http://localhost:8787";
560
- const token = process.env.SEEKRIT_TOKEN ?? config.token;
561
- const devUser = process.env.SEEKRIT_DEV_USER ?? config.devUser;
575
+ const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
576
+ const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
577
+ const token = fromEnv("SEEKRIT_TOKEN") ?? config.token;
578
+ const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
562
579
  let auth;
563
580
  if (token) auth = {
564
581
  type: "bearer",
@@ -1018,8 +1035,10 @@ async function materialize(ctx, options) {
1018
1035
  */
1019
1036
  async function materializeForRun(options) {
1020
1037
  const envFiles = options.envFile ?? [".env"];
1038
+ const dotenvVars = {};
1039
+ overlayEnvFiles(dotenvVars, {}, envFiles);
1021
1040
  try {
1022
- const ctx = tryBuildContext();
1041
+ const ctx = tryBuildContext(dotenvVars);
1023
1042
  if (!ctx) throw new Error("no credentials found (set SEEKRIT_TOKEN / SEEKRIT_DEV_USER or run `seekrit login`)");
1024
1043
  return await materialize(ctx, options);
1025
1044
  } catch (err) {
@@ -1102,16 +1121,21 @@ program.command("grant").description("give a member or service token access to a
1102
1121
  console.error(`granted ${label} access to ${principalId}`);
1103
1122
  });
1104
1123
  const token = program.command("token").description("manage service tokens (CI, docker, agents)");
1105
- token.command("create").description("create a service token bound to an app environment; prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").requiredOption("--app <slug>", "application to bind the token to").requiredOption("--env <slug>", "environment to bind the token to").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
1124
+ token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
1106
1125
  const ctx = buildContext();
1107
- const target = await resolveAppEnv(ctx, options);
1126
+ const role = options.admin ? "admin" : "member";
1127
+ const boundToEnv = Boolean(options.app || options.env);
1128
+ if (!options.admin && !boundToEnv) fail("runtime tokens need --app and --env (or pass --admin for an org-scoped token)");
1129
+ if (boundToEnv && !(options.app && options.env)) fail("pass both --app and --env to bind a token to an environment");
1130
+ const target = boundToEnv ? await resolveAppEnv(ctx, options) : { orgId: (await resolveOrg(ctx, options.org)).id };
1108
1131
  const created = await createServiceToken();
1109
1132
  await ctx.client.createToken(target.orgId, {
1110
1133
  name: options.name,
1111
1134
  tokenId: created.tokenId,
1112
1135
  tokenHash: created.tokenHash,
1113
1136
  publicKeyJwk: created.publicKeyJwk,
1114
- environmentId: target.envId
1137
+ role,
1138
+ environmentId: "envId" in target ? target.envId : null
1115
1139
  });
1116
1140
  const grantEnv = async (envId) => {
1117
1141
  const dek = await getDek(ctx, target.orgId, envId);
@@ -1121,7 +1145,8 @@ token.command("create").description("create a service token bound to an app envi
1121
1145
  wrappedDek: await wrapDek(dek, created.publicKeyJwk)
1122
1146
  });
1123
1147
  };
1124
- if (options.grant !== false) {
1148
+ const granted = boundToEnv && options.grant !== false;
1149
+ if (granted && "envId" in target) {
1125
1150
  await grantEnv(target.envId);
1126
1151
  const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
1127
1152
  for (const g of groups) {
@@ -1141,7 +1166,8 @@ token.command("create").description("create a service token bound to an app envi
1141
1166
  await grantEnv(slice.id);
1142
1167
  }
1143
1168
  }
1144
- console.error(`token created${options.grant !== false ? " and granted" : ""} for ${target.appSlug}/${target.envSlug} save it now, it is not stored:`);
1169
+ const scope = "appSlug" in target ? `${target.appSlug}/${target.envSlug}` : "org-scoped (admin)";
1170
+ console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
1145
1171
  console.log(created.token);
1146
1172
  });
1147
1173
  token.command("list").description("list service tokens").option("--org <slug>").action(async (options) => {
@@ -1159,6 +1185,10 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
1159
1185
  await ctx.client.revokeToken(orgRef.id, tokenId);
1160
1186
  console.error(`${tokenId} revoked`);
1161
1187
  });
1188
+ program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
1189
+ const { runMcpServer } = await import("./mcp-hxTidFyj.js");
1190
+ await runMcpServer();
1191
+ });
1162
1192
  program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
1163
1193
  const ctx = buildContext();
1164
1194
  const orgRef = await resolveOrg(ctx, options.org);
@@ -1169,4 +1199,4 @@ program.parseAsync().catch((err) => {
1169
1199
  fail(err instanceof Error ? err.message : String(err));
1170
1200
  });
1171
1201
  //#endregion
1172
- export {};
1202
+ export { parseServiceToken as _, encryptAndSetSecret as a, getDek as c, setFailThrows as d, writeProjectConfig as f, isServiceToken as g, createServiceToken as h, resolveOrg as i, isTokenAuth as l, wrapDek as m, resolveEnvTarget as n, fetchDecryptedSecrets as o, version as p, resolveGroup as r, materializeEnv as s, resolveAppEnv as t, tryBuildContext as u, generateDek as v };
@@ -0,0 +1,558 @@
1
+ import { _ as parseServiceToken, a as encryptAndSetSecret, c as getDek, d as setFailThrows, f as writeProjectConfig, g as isServiceToken, h as createServiceToken, i as resolveOrg, l as isTokenAuth, m as wrapDek, n as resolveEnvTarget, o as fetchDecryptedSecrets, p as version, r as resolveGroup, s as materializeEnv, t as resolveAppEnv, u as tryBuildContext, v as generateDek } from "./index.js";
2
+ import { spawn } from "node:child_process";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ //#region src/mcp.ts
7
+ function jsonText(data) {
8
+ return { content: [{
9
+ type: "text",
10
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
11
+ }] };
12
+ }
13
+ function errText(err) {
14
+ return {
15
+ content: [{
16
+ type: "text",
17
+ text: `error: ${err instanceof Error ? err.message : String(err)}`
18
+ }],
19
+ isError: true
20
+ };
21
+ }
22
+ /** Build the client context or throw a friendly, agent-readable error. */
23
+ function getCtx() {
24
+ const ctx = tryBuildContext();
25
+ 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");
26
+ return ctx;
27
+ }
28
+ /**
29
+ * Guard tools that decrypt: under user auth we cannot prompt for a passphrase
30
+ * (stdin is the transport), so it must be supplied out-of-band.
31
+ */
32
+ function ensureDecryptable(ctx) {
33
+ if (!isTokenAuth(ctx) && !process.env.SEEKRIT_PASSPHRASE) throw new Error("this operation decrypts data; set SEEKRIT_PASSPHRASE in the MCP server env, or use a service token (SEEKRIT_TOKEN)");
34
+ }
35
+ /**
36
+ * The calling principal's public key JWK — needed to wrap a freshly generated
37
+ * DEK when creating an environment. Service tokens carry their private key, so
38
+ * we derive the public half from it; users publish theirs at key setup.
39
+ */
40
+ async function principalPublicKeyJwk(ctx) {
41
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
42
+ const { privateKey } = await parseServiceToken(ctx.auth.token);
43
+ const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
44
+ return JSON.stringify(pub);
45
+ }
46
+ const { user } = await ctx.client.me();
47
+ if (!user.publicKeyJwk) throw new Error("this user has no keypair yet — run `seekrit keys setup` first");
48
+ return user.publicKeyJwk;
49
+ }
50
+ /** Run a child command with `env` and capture its output (values never returned). */
51
+ function runChild(cmd, args, env, cwd) {
52
+ return new Promise((resolve, reject) => {
53
+ const child = spawn(cmd, args, {
54
+ env,
55
+ cwd,
56
+ stdio: [
57
+ "ignore",
58
+ "pipe",
59
+ "pipe"
60
+ ]
61
+ });
62
+ let stdout = "";
63
+ let stderr = "";
64
+ const cap = 64e3;
65
+ child.stdout.on("data", (d) => {
66
+ if (stdout.length < cap) stdout += d.toString("utf8");
67
+ });
68
+ child.stderr.on("data", (d) => {
69
+ if (stderr.length < cap) stderr += d.toString("utf8");
70
+ });
71
+ child.on("error", reject);
72
+ child.on("close", (code) => resolve({
73
+ exitCode: code ?? 1,
74
+ stdout,
75
+ stderr
76
+ }));
77
+ });
78
+ }
79
+ /** Resolve the layered environment for run/export (token path infers the env). */
80
+ async function materializeFor(ctx, o) {
81
+ let envId;
82
+ if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, o)).envId;
83
+ return materializeEnv(ctx, {
84
+ envId,
85
+ with: o.with,
86
+ envFiles: o.envFile ?? [".env"]
87
+ });
88
+ }
89
+ const targetShape = {
90
+ org: z.string().optional().describe("organization slug or id (defaults to a lone org)"),
91
+ app: z.string().optional().describe("application slug or id (or set via configure_project)"),
92
+ group: z.string().optional().describe("target a group environment instead of an app"),
93
+ env: z.string().optional().describe("environment slug or id (a service token infers its own)")
94
+ };
95
+ /**
96
+ * Resolve which environment a secret tool addresses. A service token with no
97
+ * explicit app/group targets its own bound environment (no flags needed);
98
+ * everyone else names app|group + env.
99
+ */
100
+ async function resolveTargetEnv(ctx, o) {
101
+ if (isTokenAuth(ctx) && !o.app && !o.group) {
102
+ const { scope } = await ctx.client.resolve();
103
+ return {
104
+ orgId: scope.orgId,
105
+ envId: scope.envId,
106
+ label: `${scope.appSlug}/${scope.envSlug}`
107
+ };
108
+ }
109
+ return resolveEnvTarget(ctx, o);
110
+ }
111
+ async function runMcpServer() {
112
+ setFailThrows(true);
113
+ const server = new McpServer({
114
+ name: "seekrit",
115
+ version
116
+ });
117
+ /** Register a tool whose handler returns data (serialized) or throws (→ isError). */
118
+ const tool = (name, description, shape, handler) => {
119
+ server.registerTool(name, {
120
+ description,
121
+ inputSchema: shape
122
+ }, (async (args) => {
123
+ try {
124
+ return jsonText(await handler(args));
125
+ } catch (err) {
126
+ return errText(err);
127
+ }
128
+ }));
129
+ };
130
+ tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", {}, async () => {
131
+ const ctx = getCtx();
132
+ if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
133
+ const { tokenId } = await parseServiceToken(ctx.auth.token);
134
+ const { orgs } = await ctx.client.listOrgs();
135
+ let scope = null;
136
+ try {
137
+ scope = (await ctx.client.resolve()).scope;
138
+ } catch {}
139
+ return {
140
+ kind: "service_token",
141
+ tokenId,
142
+ org: orgs[0]?.slug ?? null,
143
+ boundScope: scope
144
+ };
145
+ }
146
+ return {
147
+ kind: "user",
148
+ ...await ctx.client.me()
149
+ };
150
+ });
151
+ tool("list_orgs", "List organizations the caller can access.", {}, async () => (await getCtx().client.listOrgs()).orgs);
152
+ tool("list_apps", "List applications in an organization.", { org: z.string().optional() }, async ({ org }) => {
153
+ const ctx = getCtx();
154
+ const orgRef = await resolveOrg(ctx, org);
155
+ return (await ctx.client.listApps(orgRef.id)).apps;
156
+ });
157
+ tool("list_envs", "List environments of an application.", {
158
+ org: z.string().optional(),
159
+ app: z.string()
160
+ }, async ({ org, app }) => {
161
+ const ctx = getCtx();
162
+ const orgRef = await resolveOrg(ctx, org);
163
+ const { apps } = await ctx.client.listApps(orgRef.id);
164
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
165
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
166
+ return (await ctx.client.listEnvs(orgRef.id, appRow.id)).environments;
167
+ });
168
+ tool("list_groups", "List shared groups (reusable secret bags) in an organization.", { org: z.string().optional() }, async ({ org }) => {
169
+ const ctx = getCtx();
170
+ const orgRef = await resolveOrg(ctx, org);
171
+ return (await ctx.client.listGroups(orgRef.id)).groups;
172
+ });
173
+ tool("list_group_envs", "List a group's environments (per-slug value sets).", {
174
+ org: z.string().optional(),
175
+ group: z.string()
176
+ }, async ({ org, group }) => {
177
+ const ctx = getCtx();
178
+ const g = await resolveGroup(ctx, {
179
+ org,
180
+ group
181
+ });
182
+ return (await ctx.client.listGroupEnvs(g.orgId, g.id)).environments;
183
+ });
184
+ tool("list_env_groups", "List the groups composed into an application environment (precedence order).", {
185
+ org: z.string().optional(),
186
+ app: z.string(),
187
+ env: z.string()
188
+ }, async ({ org, app, env }) => {
189
+ const ctx = getCtx();
190
+ const target = await resolveAppEnv(ctx, {
191
+ org,
192
+ app,
193
+ env
194
+ });
195
+ return (await ctx.client.listEnvGroups(target.orgId, target.envId)).groups;
196
+ });
197
+ tool("list_members", "List organization members and their public keys (for granting access).", { org: z.string().optional() }, async ({ org }) => {
198
+ const ctx = getCtx();
199
+ const orgRef = await resolveOrg(ctx, org);
200
+ return (await ctx.client.listMembers(orgRef.id)).members;
201
+ });
202
+ tool("list_secrets", "List secret names + versions in an environment (never values).", targetShape, async (o) => {
203
+ const ctx = getCtx();
204
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
205
+ const { secrets } = await ctx.client.listSecrets(orgId, envId);
206
+ return secrets.map((s) => ({
207
+ name: s.name,
208
+ version: s.version,
209
+ updatedAt: s.updatedAt
210
+ }));
211
+ });
212
+ tool("list_tokens", "List an organization's service tokens (never the secret token strings).", { org: z.string().optional() }, async ({ org }) => {
213
+ const ctx = getCtx();
214
+ const orgRef = await resolveOrg(ctx, org);
215
+ return (await ctx.client.listTokens(orgRef.id)).tokens;
216
+ });
217
+ tool("audit", "Read the organization's audit trail (most recent first).", {
218
+ org: z.string().optional(),
219
+ limit: z.number().int().min(1).max(200).optional(),
220
+ action: z.string().optional().describe("filter by action, e.g. secret.updated")
221
+ }, async ({ org, limit, action }) => {
222
+ const ctx = getCtx();
223
+ const orgRef = await resolveOrg(ctx, org);
224
+ return (await ctx.client.listAudit(orgRef.id, {
225
+ limit: limit ?? 50,
226
+ action
227
+ })).entries;
228
+ });
229
+ tool("create_org", "Create an organization. Requires a user session — service tokens cannot own a Stytch org.", {
230
+ name: z.string(),
231
+ slug: z.string()
232
+ }, async ({ name, slug }) => (await getCtx().client.createOrg({
233
+ name,
234
+ slug
235
+ })).org);
236
+ tool("create_app", "Create an application in an organization.", {
237
+ org: z.string().optional(),
238
+ name: z.string(),
239
+ slug: z.string()
240
+ }, async ({ org, name, slug }) => {
241
+ const ctx = getCtx();
242
+ const orgRef = await resolveOrg(ctx, org);
243
+ return (await ctx.client.createApp(orgRef.id, {
244
+ name,
245
+ slug
246
+ })).app;
247
+ });
248
+ tool("create_group", "Create a shared group (reusable secret bag) in an organization.", {
249
+ org: z.string().optional(),
250
+ name: z.string(),
251
+ slug: z.string()
252
+ }, async ({ org, name, slug }) => {
253
+ const ctx = getCtx();
254
+ const orgRef = await resolveOrg(ctx, org);
255
+ return (await ctx.client.createGroup(orgRef.id, {
256
+ name,
257
+ slug
258
+ })).group;
259
+ });
260
+ tool("create_env", "Create an application environment. Generates the data key locally and grants it to the caller.", {
261
+ org: z.string().optional(),
262
+ app: z.string(),
263
+ name: z.string(),
264
+ slug: z.string()
265
+ }, async ({ org, app, name, slug }) => {
266
+ const ctx = getCtx();
267
+ const orgRef = await resolveOrg(ctx, org);
268
+ const { apps } = await ctx.client.listApps(orgRef.id);
269
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
270
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
271
+ const wrappedDek = await wrapDek(generateDek(), await principalPublicKeyJwk(ctx));
272
+ return (await ctx.client.createEnv(orgRef.id, appRow.id, {
273
+ name,
274
+ slug,
275
+ wrappedDek
276
+ })).environment;
277
+ });
278
+ tool("create_group_env", "Create a group environment. Generates the data key locally and grants it to the caller.", {
279
+ org: z.string().optional(),
280
+ group: z.string(),
281
+ name: z.string(),
282
+ slug: z.string()
283
+ }, async ({ org, group, name, slug }) => {
284
+ const ctx = getCtx();
285
+ const g = await resolveGroup(ctx, {
286
+ org,
287
+ group
288
+ });
289
+ const wrappedDek = await wrapDek(generateDek(), await principalPublicKeyJwk(ctx));
290
+ return (await ctx.client.createGroupEnv(g.orgId, g.id, {
291
+ name,
292
+ slug,
293
+ wrappedDek
294
+ })).environment;
295
+ });
296
+ tool("compose_group", "Compose a group into an application environment (higher position wins on name clashes).", {
297
+ org: z.string().optional(),
298
+ app: z.string(),
299
+ env: z.string(),
300
+ group: z.string(),
301
+ position: z.number().int().min(0).optional()
302
+ }, async ({ org, app, env, group, position }) => {
303
+ const ctx = getCtx();
304
+ const target = await resolveAppEnv(ctx, {
305
+ org,
306
+ app,
307
+ env
308
+ });
309
+ const g = await resolveGroup(ctx, {
310
+ org,
311
+ group
312
+ });
313
+ return (await ctx.client.linkEnvGroup(target.orgId, target.envId, {
314
+ groupId: g.id,
315
+ position
316
+ })).group;
317
+ });
318
+ tool("uncompose_group", "Remove a composed group from an application environment.", {
319
+ org: z.string().optional(),
320
+ app: z.string(),
321
+ env: z.string(),
322
+ group: z.string()
323
+ }, async ({ org, app, env, group }) => {
324
+ const ctx = getCtx();
325
+ const target = await resolveAppEnv(ctx, {
326
+ org,
327
+ app,
328
+ env
329
+ });
330
+ const g = await resolveGroup(ctx, {
331
+ org,
332
+ group
333
+ });
334
+ await ctx.client.unlinkEnvGroup(target.orgId, target.envId, g.id);
335
+ return { ok: true };
336
+ });
337
+ tool("set_secret", "Encrypt a value locally and store it in an environment.", {
338
+ ...targetShape,
339
+ name: z.string(),
340
+ value: z.string()
341
+ }, async (o) => {
342
+ const ctx = getCtx();
343
+ ensureDecryptable(ctx);
344
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
345
+ await encryptAndSetSecret(ctx, orgId, envId, o.name, o.value);
346
+ return {
347
+ ok: true,
348
+ name: o.name
349
+ };
350
+ });
351
+ 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).", {
352
+ ...targetShape,
353
+ name: z.string(),
354
+ reveal: z.boolean().optional()
355
+ }, async (o) => {
356
+ const ctx = getCtx();
357
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
358
+ if (!o.reveal) {
359
+ const { secrets } = await ctx.client.listSecrets(orgId, envId);
360
+ const row = secrets.find((s) => s.name === o.name);
361
+ if (!row) throw new Error(`no secret named ${o.name}`);
362
+ return {
363
+ name: row.name,
364
+ version: row.version,
365
+ revealed: false
366
+ };
367
+ }
368
+ ensureDecryptable(ctx);
369
+ const values = await fetchDecryptedSecrets(ctx, orgId, envId);
370
+ if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
371
+ return {
372
+ name: o.name,
373
+ value: values[o.name],
374
+ revealed: true
375
+ };
376
+ });
377
+ tool("delete_secret", "Delete a secret from an environment.", {
378
+ ...targetShape,
379
+ name: z.string()
380
+ }, async (o) => {
381
+ const ctx = getCtx();
382
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
383
+ await ctx.client.deleteSecret(orgId, envId, o.name);
384
+ return {
385
+ ok: true,
386
+ name: o.name
387
+ };
388
+ });
389
+ tool("run_command", "Run a command with the resolved secrets injected as environment variables, and return its exit code + captured output. Secret VALUES are never returned — this is the preferred way to use secrets. process env > .env > app env > groups.", {
390
+ command: z.string().describe("executable to run"),
391
+ args: z.array(z.string()).optional(),
392
+ org: z.string().optional(),
393
+ app: z.string().optional(),
394
+ env: z.string().optional().describe("environment slug (token auth infers this)"),
395
+ with: z.record(z.string(), z.string()).optional().describe("group=env slice overrides"),
396
+ envFile: z.array(z.string()).optional().describe(".env files to overlay (default [.env])"),
397
+ cwd: z.string().optional()
398
+ }, async (o) => {
399
+ const ctx = getCtx();
400
+ ensureDecryptable(ctx);
401
+ const { values } = await materializeFor(ctx, o);
402
+ const childEnv = {
403
+ ...values,
404
+ ...process.env
405
+ };
406
+ return {
407
+ ...await runChild(o.command, o.args ?? [], childEnv, o.cwd),
408
+ injectedVarCount: Object.keys(values).length
409
+ };
410
+ });
411
+ tool("export_env", "Write the resolved secrets to a dotenv file on disk and return the variable names written (never the values). Use to materialize a .env for local tooling.", {
412
+ file: z.string().describe("path to write, e.g. .env"),
413
+ org: z.string().optional(),
414
+ app: z.string().optional(),
415
+ env: z.string().optional(),
416
+ with: z.record(z.string(), z.string()).optional()
417
+ }, async (o) => {
418
+ const ctx = getCtx();
419
+ ensureDecryptable(ctx);
420
+ const { values } = await materializeFor(ctx, o);
421
+ const { writeFileSync } = await import("node:fs");
422
+ const body = Object.entries(values).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("\n");
423
+ writeFileSync(o.file, `${body}\n`, { mode: 384 });
424
+ return {
425
+ file: o.file,
426
+ names: Object.keys(values).sort()
427
+ };
428
+ });
429
+ tool("create_token", "Mint a service token, printed once. Runtime tokens bind to one app environment (auto-granted its keys, so a command/agent can decrypt it). Pass admin:true for an org-scoped provisioning token (create apps/groups/envs, grant, mint tokens) — admin tokens need no env binding.", {
430
+ name: z.string().describe("display name, e.g. ci-deploy or agent-session"),
431
+ org: z.string().optional(),
432
+ app: z.string().optional().describe("bind to this app (runtime tokens)"),
433
+ env: z.string().optional().describe("bind to this env (runtime tokens)"),
434
+ admin: z.boolean().optional().describe("mint an org-scoped admin token")
435
+ }, async ({ name, org, app, env, admin }) => {
436
+ const ctx = getCtx();
437
+ const role = admin ? "admin" : "member";
438
+ const boundToEnv = Boolean(app || env);
439
+ if (!admin && !boundToEnv) throw new Error("runtime tokens need app + env, or pass admin:true for an org-scoped token");
440
+ if (boundToEnv && !(app && env)) throw new Error("pass both app and env to bind a token");
441
+ const target = boundToEnv ? await resolveAppEnv(ctx, {
442
+ org,
443
+ app,
444
+ env
445
+ }) : { orgId: (await resolveOrg(ctx, org)).id };
446
+ const created = await createServiceToken();
447
+ await ctx.client.createToken(target.orgId, {
448
+ name,
449
+ tokenId: created.tokenId,
450
+ tokenHash: created.tokenHash,
451
+ publicKeyJwk: created.publicKeyJwk,
452
+ role,
453
+ environmentId: "envId" in target ? target.envId : null
454
+ });
455
+ if (boundToEnv && "envId" in target) {
456
+ ensureDecryptable(ctx);
457
+ const grantEnv = async (envId) => {
458
+ const dek = await getDek(ctx, target.orgId, envId);
459
+ await ctx.client.grantEnvKey(target.orgId, envId, {
460
+ principalType: "service_token",
461
+ principalId: created.tokenId,
462
+ wrappedDek: await wrapDek(dek, created.publicKeyJwk)
463
+ });
464
+ };
465
+ await grantEnv(target.envId);
466
+ const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
467
+ for (const g of groups) {
468
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
469
+ const slice = environments.find((e) => e.slug === target.envSlug);
470
+ if (slice) await grantEnv(slice.id);
471
+ }
472
+ }
473
+ return {
474
+ token: created.token,
475
+ tokenId: created.tokenId,
476
+ role,
477
+ note: "save this now — the secret token string is not stored and cannot be retrieved"
478
+ };
479
+ });
480
+ tool("revoke_token", "Revoke a service token by id.", {
481
+ org: z.string().optional(),
482
+ tokenId: z.string()
483
+ }, async ({ org, tokenId }) => {
484
+ const ctx = getCtx();
485
+ const orgRef = await resolveOrg(ctx, org);
486
+ await ctx.client.revokeToken(orgRef.id, tokenId);
487
+ return {
488
+ ok: true,
489
+ tokenId
490
+ };
491
+ });
492
+ tool("grant_env", "Grant a member (by email) or service token (by id) access to an environment's data key. Re-wraps the DEK to the grantee — the caller must already hold the key.", {
493
+ ...targetShape,
494
+ user: z.string().optional().describe("org member email"),
495
+ token: z.string().optional().describe("service token id (skt_…)")
496
+ }, async (o) => {
497
+ if (Boolean(o.user) === Boolean(o.token)) throw new Error("pass exactly one of user or token");
498
+ const ctx = getCtx();
499
+ ensureDecryptable(ctx);
500
+ const { orgId, envId, label } = await resolveTargetEnv(ctx, o);
501
+ const dek = await getDek(ctx, orgId, envId);
502
+ let principalType;
503
+ let principalId;
504
+ let publicKeyJwk;
505
+ if (o.user) {
506
+ const { members } = await ctx.client.listMembers(orgId);
507
+ const member = members.find((m) => m.email === o.user);
508
+ if (!member) throw new Error(`no member ${o.user}`);
509
+ if (!member.publicKeyJwk) throw new Error(`${o.user} has not completed key setup`);
510
+ [principalType, principalId, publicKeyJwk] = [
511
+ "user",
512
+ member.userId,
513
+ member.publicKeyJwk
514
+ ];
515
+ } else {
516
+ const { tokens } = await ctx.client.listTokens(orgId);
517
+ const t = tokens.find((x) => x.id === o.token);
518
+ if (!t) throw new Error(`no service token ${o.token}`);
519
+ [principalType, principalId, publicKeyJwk] = [
520
+ "service_token",
521
+ t.id,
522
+ t.publicKeyJwk
523
+ ];
524
+ }
525
+ await ctx.client.grantEnvKey(orgId, envId, {
526
+ principalType,
527
+ principalId,
528
+ wrappedDek: await wrapDek(dek, publicKeyJwk)
529
+ });
530
+ return {
531
+ ok: true,
532
+ env: label,
533
+ principalType,
534
+ principalId
535
+ };
536
+ });
537
+ tool("configure_project", "Link a directory to an org/app by writing seekrit.json (like `seekrit init`). The environment is chosen by the service token at runtime.", {
538
+ org: z.string(),
539
+ app: z.string(),
540
+ dir: z.string().optional()
541
+ }, async ({ org, app, dir }) => {
542
+ const ctx = getCtx();
543
+ const orgRef = await resolveOrg(ctx, org);
544
+ const { apps } = await ctx.client.listApps(orgRef.id);
545
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
546
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
547
+ return {
548
+ path: writeProjectConfig({
549
+ org: orgRef.slug,
550
+ app: appRow.slug
551
+ }, dir),
552
+ guidance: "Run commands with `seekrit run -- <cmd>` (or the seekrit-run binary) and a SEEKRIT_TOKEN bound to the desired environment; the token selects org+app+env."
553
+ };
554
+ });
555
+ await server.connect(new StdioServerTransport());
556
+ }
557
+ //#endregion
558
+ export { runMcpServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -21,7 +21,9 @@
21
21
  "typecheck": "tsc --noEmit"
22
22
  },
23
23
  "dependencies": {
24
- "commander": "^15.0.0"
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "commander": "^15.0.0",
26
+ "zod": "^4.4.3"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@seekrit/api-client": "workspace:*",