@icoretech/warden-mcp 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -62,12 +62,14 @@ BW_BIN=/absolute/path/to/bw npx -y @icoretech/warden-mcp@latest
62
62
  `warden-mcp` intentionally bundles a vetted `@bitwarden/cli` version instead of
63
63
  blindly following the newest upstream CLI on every release. New `bw` releases
64
64
  can change login and unlock behavior in ways that break automation, so `bw`
65
- upgrades should be smoke-tested against real Vaultwarden and Bitwarden flows
66
- before bumping the bundled version.
65
+ upgrades should be smoke-tested against real Vaultwarden flows before bumping
66
+ the bundled version. Official Bitwarden compatibility is intended, but it is
67
+ not continuously proven in CI without a real Bitwarden tenant.
67
68
 
68
- This repository's compose smoke now exercises both username/password auth and
69
- user API-key auth against a real local Vaultwarden so `@bitwarden/cli` bumps do
70
- not rely on unit coverage alone.
69
+ This repository's compose smoke now exercises both direct `bw` auth flows and
70
+ the MCP/SDK layers with username/password auth plus user API-key auth against a
71
+ real local Vaultwarden, so `@bitwarden/cli` bumps do not rely on unit coverage
72
+ alone.
71
73
 
72
74
  ## Install And Run
73
75
 
@@ -456,7 +458,8 @@ Run integration tests:
456
458
  make test
457
459
  ```
458
460
 
459
- `make test` now runs both compose-backed auth paths:
461
+ `make test` now runs both compose-backed auth paths and verifies them at the
462
+ raw CLI plus MCP/SDK layers:
460
463
 
461
464
  - user/password login from `.env.test`
462
465
  - api-key login from `tmp/vaultwarden-bootstrap/apikey.env`, generated by the bootstrap step and kept out of git via `tmp/`
package/bin/warden-mcp.js CHANGED
@@ -3,8 +3,7 @@
3
3
  // bin/warden-mcp.js — CLI entry for @icoretech/warden-mcp
4
4
 
5
5
  import { spawnSync } from 'node:child_process';
6
- import { accessSync, constants, existsSync, readFileSync } from 'node:fs';
7
- import { createRequire } from 'node:module';
6
+ import { existsSync } from 'node:fs';
8
7
  import { dirname, resolve } from 'node:path';
9
8
  import { fileURLToPath } from 'node:url';
10
9
 
@@ -13,20 +12,12 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
13
12
  // Resolve bw binary: optional dep → system PATH
14
13
  if (!process.env.BW_BIN) {
15
14
  try {
16
- const require = createRequire(import.meta.url);
17
- const { resolveBundledBwCandidate } = await import(
15
+ const { resolveBundledBwBin } = await import(
18
16
  resolve(__dirname, '../dist/bw/resolveBwBin.js')
19
17
  );
20
- const pkgManifest = require.resolve('@bitwarden/cli/package.json');
21
- const pkgJson = JSON.parse(readFileSync(pkgManifest, 'utf8'));
22
- const candidate = resolveBundledBwCandidate(pkgManifest, pkgJson.bin);
23
- if (existsSync(candidate)) {
24
- try {
25
- accessSync(candidate, constants.X_OK);
26
- process.env.BW_BIN = candidate;
27
- } catch {
28
- // Not executable — fall through to system bw
29
- }
18
+ const candidate = resolveBundledBwBin();
19
+ if (candidate) {
20
+ process.env.BW_BIN = candidate;
30
21
  }
31
22
  } catch {
32
23
  // @bitwarden/cli optional dep not installed — fall through to system bw
package/dist/bw/bwCli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/bw/bwCli.ts
2
2
  import { spawn } from 'node:child_process';
3
+ import { resolveBundledBwBin } from './resolveBwBin.js';
3
4
  export class BwCliError extends Error {
4
5
  exitCode;
5
6
  stdout;
@@ -13,7 +14,7 @@ export class BwCliError extends Error {
13
14
  }
14
15
  }
15
16
  export async function runBw(args, opts = {}) {
16
- const bwBin = process.env.BW_BIN ?? 'bw';
17
+ const bwBin = process.env.BW_BIN ?? resolveBundledBwBin() ?? 'bw';
17
18
  // Ensure the CLI never blocks waiting for a prompt (e.g. master password).
18
19
  // This is critical for running as an MCP server / in test automation.
19
20
  const injectNoInteraction = opts.noInteraction ?? true;
@@ -3,6 +3,11 @@ import { rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { runBw } from './bwCli.js';
5
5
  import { Mutex } from './mutex.js';
6
+ const POST_LOGIN_UNLOCK_RETRY_ATTEMPTS = 20;
7
+ const POST_LOGIN_UNLOCK_RETRY_DELAY_MS = 2_000;
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
6
11
  function requiredEnv(name) {
7
12
  const v = process.env[name];
8
13
  if (!v) {
@@ -188,7 +193,7 @@ export class BwSessionManager {
188
193
  timeoutMs: 60_000,
189
194
  noInteraction: false,
190
195
  });
191
- return stdout.trim();
196
+ return { completed: true, session: stdout.trim() };
192
197
  }
193
198
  const { stdout } = await runBw([
194
199
  'login',
@@ -197,17 +202,35 @@ export class BwSessionManager {
197
202
  'BW_PASSWORD',
198
203
  '--raw',
199
204
  ], { env: unlockEnv, timeoutMs: 60_000, noInteraction: false });
200
- return stdout.trim();
205
+ return { completed: true, session: stdout.trim() };
201
206
  }
202
207
  catch {
203
- return '';
208
+ return { completed: false, session: '' };
209
+ }
210
+ };
211
+ const retryUnlockAfterLogin = async () => {
212
+ for (let attempt = 0; attempt < POST_LOGIN_UNLOCK_RETRY_ATTEMPTS; attempt += 1) {
213
+ const session = await tryUnlock();
214
+ if (session)
215
+ return session;
216
+ if (attempt < POST_LOGIN_UNLOCK_RETRY_ATTEMPTS - 1) {
217
+ await sleep(POST_LOGIN_UNLOCK_RETRY_DELAY_MS);
218
+ }
204
219
  }
220
+ return '';
205
221
  };
206
222
  // Prefer unlocking first (works when already logged in). If it yields an empty
207
223
  // stdout on exit=0 (observed in some bw builds), fall back to login --raw.
208
224
  let session = await tryUnlock();
209
- if (!session)
210
- session = await tryLoginRaw();
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();
232
+ }
233
+ }
211
234
  if (!session)
212
235
  session = await tryUnlock();
213
236
  if (!session)
@@ -1,3 +1,5 @@
1
+ import { accessSync, constants, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
1
3
  import { dirname, join } from 'node:path';
2
4
  export function resolveBundledBwCandidate(pkgManifestPath, pkgBin) {
3
5
  const pkgDir = dirname(pkgManifestPath);
@@ -8,3 +10,15 @@ export function resolveBundledBwCandidate(pkgManifestPath, pkgBin) {
8
10
  : 'dist/bw';
9
11
  return join(pkgDir, binEntry);
10
12
  }
13
+ export function resolveBundledBwBin(resolvePackage = createRequire(import.meta.url).resolve) {
14
+ try {
15
+ const pkgManifestPath = resolvePackage('@bitwarden/cli/package.json');
16
+ const pkgJson = JSON.parse(readFileSync(pkgManifestPath, 'utf8'));
17
+ const candidate = resolveBundledBwCandidate(pkgManifestPath, pkgJson.bin);
18
+ accessSync(candidate, constants.X_OK);
19
+ return candidate;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@icoretech/warden-mcp",
4
- "version": "0.1.15",
4
+ "version": "0.1.17",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "description": "Vaultwarden/Bitwarden MCP server backed by Bitwarden CLI (bw).",