@icoretech/warden-mcp 0.1.17 → 0.1.18

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.
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { createRequire } from 'node:module';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const rootDir = resolve(__dirname, '..');
12
+ const cliPackageDir = resolve(__dirname, '../node_modules/@bitwarden/cli');
13
+
14
+ if (!existsSync(cliPackageDir)) {
15
+ process.exit(0);
16
+ }
17
+
18
+ const patchPackageEntrypoint = require.resolve('patch-package/dist/index.js');
19
+ const result = spawnSync(process.execPath, [patchPackageEntrypoint], {
20
+ cwd: rootDir,
21
+ stdio: 'inherit',
22
+ });
23
+
24
+ if (result.error) {
25
+ console.error(
26
+ `[warden-mcp] failed to execute patch-package: ${result.error.message}`,
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ process.exit(result.status ?? 1);
package/bin/warden-mcp.js CHANGED
@@ -2,41 +2,21 @@
2
2
 
3
3
  // bin/warden-mcp.js — CLI entry for @icoretech/warden-mcp
4
4
 
5
- import { spawnSync } from 'node:child_process';
6
5
  import { existsSync } from 'node:fs';
7
6
  import { dirname, resolve } from 'node:path';
8
7
  import { fileURLToPath } from 'node:url';
9
8
 
10
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
10
 
12
- // Resolve bw binary: optional dep → system PATH
13
- if (!process.env.BW_BIN) {
14
- try {
15
- const { resolveBundledBwBin } = await import(
16
- resolve(__dirname, '../dist/bw/resolveBwBin.js')
17
- );
18
- const candidate = resolveBundledBwBin();
19
- if (candidate) {
20
- process.env.BW_BIN = candidate;
21
- }
22
- } catch {
23
- // @bitwarden/cli optional dep not installed — fall through to system bw
24
- }
25
- }
26
-
27
- // Verify bw is available (either from optional dep or system PATH)
28
- if (!process.env.BW_BIN) {
29
- const probe = spawnSync('bw', ['--version'], { encoding: 'utf8' });
30
- if (probe.error) {
31
- console.error(
32
- '[warden-mcp] ERROR: bw CLI not found.\n' +
33
- 'Install it with: npm install -g @bitwarden/cli\n' +
34
- 'Or set the BW_BIN environment variable to the path of the bw binary.',
35
- );
36
- process.exit(1);
37
- }
38
- // System bw is available — bwCli.ts will find it via PATH
11
+ const startupPath = resolve(__dirname, '../dist/startup/bwStartup.js');
12
+ if (!existsSync(startupPath)) {
13
+ console.error(
14
+ '[warden-mcp] ERROR: dist/startup/bwStartup.js not found. Run `npm run build` first.',
15
+ );
16
+ process.exit(1);
39
17
  }
18
+ const { prepareBwStartup } = await import(startupPath);
19
+ prepareBwStartup(process.env);
40
20
 
41
21
  // Delegate to the compiled server entry, forwarding all arguments.
42
22
  const serverPath = resolve(__dirname, '../dist/server.js');
package/dist/bw/bwCli.js CHANGED
@@ -81,7 +81,14 @@ export async function runBw(args, opts = {}) {
81
81
  }, timeoutMs);
82
82
  });
83
83
  const completed = new Promise((resolve, reject) => {
84
- child.on('error', reject);
84
+ child.on('error', (error) => {
85
+ const safeCmd = `${bwBin} ${safeRenderedArgs(finalArgs)}`;
86
+ if (error.code === 'ENOENT') {
87
+ reject(new Error(`bw CLI not available for ${safeCmd}. Install @bitwarden/cli or set BW_BIN to a valid bw binary.`));
88
+ return;
89
+ }
90
+ reject(new Error(`Failed to start ${safeCmd}: ${error.message}`));
91
+ });
85
92
  child.on('close', (code) => {
86
93
  if (timeout)
87
94
  clearTimeout(timeout);
@@ -150,6 +150,19 @@ export class BwSessionManager {
150
150
  env: this.baseEnv(opts.env),
151
151
  });
152
152
  }
153
+ async resetCliProfile() {
154
+ this.session = null;
155
+ this.templateItem = null;
156
+ this.configuredHost = null;
157
+ await runBw(['logout'], { env: this.baseEnv(), timeoutMs: 30_000 }).catch(() => { });
158
+ const home = this.homeDir;
159
+ await rm(join(home, '.config', 'Bitwarden CLI', 'data.json'), {
160
+ force: true,
161
+ }).catch(() => { });
162
+ await rm(join(home, '.config', 'Bitwarden CLI', 'config.json'), {
163
+ force: true,
164
+ }).catch(() => { });
165
+ }
153
166
  async ensureUnlockedInternal() {
154
167
  // Ensure server config points to BW_HOST.
155
168
  await this.ensureServerConfigured();
@@ -219,20 +232,30 @@ export class BwSessionManager {
219
232
  }
220
233
  return '';
221
234
  };
222
- // Prefer unlocking first (works when already logged in). If it yields an empty
223
- // stdout on exit=0 (observed in some bw builds), fall back to login --raw.
224
- let session = await tryUnlock();
225
- if (!session) {
226
- const login = await tryLoginRaw();
227
- if (login.session) {
228
- session = login.session;
229
- }
230
- else if (login.completed) {
231
- session = await retryUnlockAfterLogin();
235
+ const obtainSession = async () => {
236
+ // Prefer unlocking first (works when already logged in). If it yields an
237
+ // empty stdout on exit=0 (observed in some bw builds), fall back to
238
+ // login --raw.
239
+ let session = await tryUnlock();
240
+ if (!session) {
241
+ const login = await tryLoginRaw();
242
+ if (login.session) {
243
+ session = login.session;
244
+ }
245
+ else if (login.completed) {
246
+ session = await retryUnlockAfterLogin();
247
+ }
232
248
  }
249
+ if (!session)
250
+ session = await tryUnlock();
251
+ return session;
252
+ };
253
+ let session = await obtainSession();
254
+ if (!session) {
255
+ await this.resetCliProfile();
256
+ await this.ensureServerConfigured();
257
+ session = await obtainSession();
233
258
  }
234
- if (!session)
235
- session = await tryUnlock();
236
259
  if (!session)
237
260
  throw new Error('bw login/unlock returned an empty session');
238
261
  this.session = session;
@@ -254,13 +277,7 @@ export class BwSessionManager {
254
277
  catch {
255
278
  // If the CLI data is corrupt/out-of-sync, wiping config is the fastest recovery.
256
279
  }
257
- const home = this.homeDir;
258
- await rm(join(home, '.config', 'Bitwarden CLI', 'data.json'), {
259
- force: true,
260
- }).catch(() => { });
261
- await rm(join(home, '.config', 'Bitwarden CLI', 'config.json'), {
262
- force: true,
263
- }).catch(() => { });
280
+ await this.resetCliProfile();
264
281
  await runBw(['config', 'server', this.env.host], {
265
282
  env: this.baseEnv(),
266
283
  timeoutMs: 30_000,
@@ -168,8 +168,85 @@ export class KeychainSdk {
168
168
  maybeRedact(value, reveal) {
169
169
  return (reveal ? value : redactItem(value));
170
170
  }
171
- valueResult(value, revealed) {
172
- return { value, revealed };
171
+ valueResult(value, revealed, extra) {
172
+ return { value, revealed, ...(extra ?? {}) };
173
+ }
174
+ extractLoginTotp(item) {
175
+ if (!item || typeof item !== 'object')
176
+ return null;
177
+ const login = item.login;
178
+ if (!login || typeof login !== 'object')
179
+ return null;
180
+ const totp = login.totp;
181
+ return typeof totp === 'string' && totp.trim().length > 0 ? totp : null;
182
+ }
183
+ computeTotpMetadata(rawTotp, nowMs = Date.now()) {
184
+ if (!rawTotp)
185
+ return { period: null, timeLeft: null };
186
+ let period = 30;
187
+ if (rawTotp.startsWith('otpauth://')) {
188
+ try {
189
+ const parsed = new URL(rawTotp);
190
+ const candidate = Number.parseInt(parsed.searchParams.get('period') ?? '', 10);
191
+ if (Number.isFinite(candidate) && candidate > 0) {
192
+ period = candidate;
193
+ }
194
+ }
195
+ catch {
196
+ return { period: null, timeLeft: null };
197
+ }
198
+ }
199
+ const elapsed = Math.floor(nowMs / 1000) % period;
200
+ return {
201
+ period,
202
+ timeLeft: elapsed === 0 ? period : period - elapsed,
203
+ };
204
+ }
205
+ candidateMatchesTerm(item, terms) {
206
+ const id = item.id;
207
+ const name = item.name;
208
+ const login = item.login && typeof item.login === 'object'
209
+ ? item.login
210
+ : null;
211
+ const username = login?.username;
212
+ return terms.some((term) => id === term || name === term || username === term);
213
+ }
214
+ async resolveTotpConfigForSession(session, term) {
215
+ const direct = await this.bw
216
+ .runForSession(session, ['get', 'item', term], { timeoutMs: 60_000 })
217
+ .then(({ stdout }) => this.parseBwJson(stdout))
218
+ .catch(() => null);
219
+ const directTotp = this.extractLoginTotp(direct);
220
+ if (directTotp)
221
+ return directTotp;
222
+ const tokens = term
223
+ .split('|')
224
+ .map((token) => token.trim())
225
+ .filter((token) => token.length > 0);
226
+ const terms = tokens.length ? tokens : [term];
227
+ const byId = new Map();
228
+ for (const searchTerm of terms) {
229
+ const { stdout } = await this.bw.runForSession(session, ['list', 'items', '--search', searchTerm], { timeoutMs: 120_000 });
230
+ const results = this.parseBwJson(stdout);
231
+ for (const raw of results) {
232
+ if (!raw || typeof raw !== 'object')
233
+ continue;
234
+ const item = raw;
235
+ if (item.type !== ITEM_TYPE.login)
236
+ continue;
237
+ const id = item.id;
238
+ if (typeof id === 'string' && id.length > 0)
239
+ byId.set(id, item);
240
+ }
241
+ }
242
+ const candidates = [...byId.values()];
243
+ const candidate = candidates.find((item) => this.candidateMatchesTerm(item, terms)) ??
244
+ candidates[0];
245
+ if (!candidate || typeof candidate.id !== 'string')
246
+ return null;
247
+ const { stdout } = await this.bw.runForSession(session, ['get', 'item', candidate.id], { timeoutMs: 60_000 });
248
+ const item = this.parseBwJson(stdout);
249
+ return this.extractLoginTotp(item);
173
250
  }
174
251
  parseBwJson(stdout) {
175
252
  try {
@@ -848,11 +925,20 @@ export class KeychainSdk {
848
925
  });
849
926
  }
850
927
  async getTotp(input, opts = {}) {
851
- if (!opts.reveal)
852
- return this.valueResult(null, false);
928
+ if (!opts.reveal) {
929
+ return {
930
+ ...this.valueResult(null, false),
931
+ period: null,
932
+ timeLeft: null,
933
+ };
934
+ }
853
935
  return this.bw.withSession(async (session) => {
854
936
  const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'totp', input.term], { timeoutMs: 60_000 });
855
- return this.valueResult(stdout.trim(), true);
937
+ const rawTotp = await this.resolveTotpConfigForSession(session, input.term).catch(() => null);
938
+ return {
939
+ ...this.valueResult(stdout.trim(), true),
940
+ ...this.computeTotpMetadata(rawTotp),
941
+ };
856
942
  });
857
943
  }
858
944
  /** Always reveals — URIs are not considered secrets by Bitwarden. */
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { resolveBundledBwBin } from '../bw/resolveBwBin.js';
3
+ export function prepareBwStartup(env = process.env, deps = {}) {
4
+ if (!env.BW_BIN) {
5
+ const resolveBundled = deps.resolveBundledBwBin ?? resolveBundledBwBin;
6
+ const candidate = resolveBundled();
7
+ if (candidate) {
8
+ env.BW_BIN = candidate;
9
+ return;
10
+ }
11
+ }
12
+ if (env.BW_BIN)
13
+ return;
14
+ const probe = (deps.probeSystemBw ?? spawnSync)('bw', ['--version'], {
15
+ encoding: 'utf8',
16
+ });
17
+ if (!probe.error)
18
+ return;
19
+ const warn = deps.warn ?? console.warn;
20
+ warn('[warden-mcp] WARNING: bw CLI not found.\n' +
21
+ 'Install it with: npm install -g @bitwarden/cli\n' +
22
+ 'Or set the BW_BIN environment variable to the path of the bw binary.\n' +
23
+ 'The server will start but tool calls will fail until bw is available.');
24
+ }
@@ -35,8 +35,8 @@ export function registerTools(server, deps) {
35
35
  function effectiveReveal(input) {
36
36
  return isNoReveal ? false : (input.reveal ?? false);
37
37
  }
38
- function toolResult(kind, value, revealed) {
39
- return { result: { kind, value, revealed } };
38
+ function toolResult(kind, value, revealed, extra) {
39
+ return { result: { kind, value, revealed, ...(extra ?? {}) } };
40
40
  }
41
41
  const uriMatchSchema = z.enum([
42
42
  'domain',
@@ -926,7 +926,10 @@ export function registerTools(server, deps) {
926
926
  const sdk = await deps.getSdk(extra.authInfo);
927
927
  const totp = await sdk.getTotp({ term: input.term }, { reveal: effectiveReveal(input) });
928
928
  return {
929
- structuredContent: toolResult('totp', totp.value, totp.revealed),
929
+ structuredContent: toolResult('totp', totp.value, totp.revealed, {
930
+ period: totp.period,
931
+ timeLeft: totp.timeLeft,
932
+ }),
930
933
  content: [{ type: 'text', text: 'OK' }],
931
934
  };
932
935
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@icoretech/warden-mcp",
4
- "version": "0.1.17",
4
+ "version": "0.1.18",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "description": "Vaultwarden/Bitwarden MCP server backed by Bitwarden CLI (bw).",
@@ -29,11 +29,13 @@
29
29
  "files": [
30
30
  "dist/",
31
31
  "bin/",
32
+ "patches/",
32
33
  "!dist/**/*.test.js",
33
34
  "!dist/integration/"
34
35
  ],
35
36
  "scripts": {
36
37
  "dev": "tsx watch --clear-screen=false src/server.ts",
38
+ "postinstall": "node bin/patch-bitwarden-cli.js",
37
39
  "build": "tsc -p .",
38
40
  "start": "node dist/server.js",
39
41
  "test": "npm run build && node --test \"dist/**/*.test.js\"",
@@ -45,10 +47,11 @@
45
47
  "dependencies": {
46
48
  "@modelcontextprotocol/sdk": "^1.27.1",
47
49
  "express": "^5.2.1",
50
+ "patch-package": "^8.0.1",
48
51
  "zod": "^4.3.6"
49
52
  },
50
53
  "optionalDependencies": {
51
- "@bitwarden/cli": "2026.1.0"
54
+ "@bitwarden/cli": "2026.2.0"
52
55
  },
53
56
  "devDependencies": {
54
57
  "@biomejs/biome": "^2.4.8",
@@ -0,0 +1,80 @@
1
+ diff --git a/node_modules/@bitwarden/cli/build/bw.js b/node_modules/@bitwarden/cli/build/bw.js
2
+ index 6b02f68..2085f5e 100755
3
+ --- a/node_modules/@bitwarden/cli/build/bw.js
4
+ +++ b/node_modules/@bitwarden/cli/build/bw.js
5
+ @@ -27093,7 +27093,17 @@ class AuthRequestLoginStrategy extends LoginStrategy {
6
+ }
7
+ setAccountCryptographicState(response, userId) {
8
+ return auth_request_login_strategy_awaiter(this, void 0, void 0, function* () {
9
+ - yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
10
+ + /* icoretech-vaultwarden-compat */
11
+ + if (response.accountKeysResponseModel) {
12
+ + yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
13
+ + }
14
+ + else if (response.privateKey) {
15
+ + yield this.accountCryptographicStateService.setAccountCryptographicState({
16
+ + V1: {
17
+ + private_key: response.privateKey,
18
+ + },
19
+ + }, userId);
20
+ + }
21
+ });
22
+ }
23
+ exportCache() {
24
+ @@ -27203,7 +27213,17 @@ class PasswordLoginStrategy extends LoginStrategy {
25
+ }
26
+ setAccountCryptographicState(response, userId) {
27
+ return password_login_strategy_awaiter(this, void 0, void 0, function* () {
28
+ - yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
29
+ + /* icoretech-vaultwarden-compat */
30
+ + if (response.accountKeysResponseModel) {
31
+ + yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
32
+ + }
33
+ + else if (response.privateKey) {
34
+ + yield this.accountCryptographicStateService.setAccountCryptographicState({
35
+ + V1: {
36
+ + private_key: response.privateKey,
37
+ + },
38
+ + }, userId);
39
+ + }
40
+ });
41
+ }
42
+ encryptionKeyMigrationRequired(response) {
43
+ @@ -28327,7 +28347,17 @@ class UserApiLoginStrategy extends LoginStrategy {
44
+ }
45
+ setAccountCryptographicState(response, userId) {
46
+ return user_api_login_strategy_awaiter(this, void 0, void 0, function* () {
47
+ - yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
48
+ + /* icoretech-vaultwarden-compat */
49
+ + if (response.accountKeysResponseModel) {
50
+ + yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
51
+ + }
52
+ + else if (response.privateKey) {
53
+ + yield this.accountCryptographicStateService.setAccountCryptographicState({
54
+ + V1: {
55
+ + private_key: response.privateKey,
56
+ + },
57
+ + }, userId);
58
+ + }
59
+ });
60
+ }
61
+ // Overridden to save client ID and secret to token service
62
+ @@ -28457,7 +28487,17 @@ class WebAuthnLoginStrategy extends LoginStrategy {
63
+ }
64
+ setAccountCryptographicState(response, userId) {
65
+ return webauthn_login_strategy_awaiter(this, void 0, void 0, function* () {
66
+ - yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
67
+ + /* icoretech-vaultwarden-compat */
68
+ + if (response.accountKeysResponseModel) {
69
+ + yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
70
+ + }
71
+ + else if (response.privateKey) {
72
+ + yield this.accountCryptographicStateService.setAccountCryptographicState({
73
+ + V1: {
74
+ + private_key: response.privateKey,
75
+ + },
76
+ + }, userId);
77
+ + }
78
+ });
79
+ }
80
+ exportCache() {