@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.
- package/dist/index.js +166 -11
- 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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
26
|
+
"@seekrit/cli": "0.24.0"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "tsdown",
|