@icoretech/warden-mcp 0.1.5 → 0.1.7

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
@@ -339,7 +339,7 @@ npm install -g @bitwarden/cli
339
339
 
340
340
  The server executes `bw` commands on your behalf:
341
341
 
342
- - In HTTP mode, Bitwarden/Vaultwarden connection + credentials are provided via **HTTP headers** per request.
342
+ - In HTTP mode, Bitwarden/Vaultwarden connection + credentials are provided via **HTTP headers** per request. Env-var fallback is disabled by default; set `KEYCHAIN_ALLOW_ENV_FALLBACK=true` to enable it for single-tenant HTTP deployments.
343
343
  - In stdio mode, Bitwarden/Vaultwarden credentials are loaded once from `BW_*` env vars at startup.
344
344
  - The server maintains per-profile `bw` state under `KEYCHAIN_BW_HOME_ROOT` to avoid session/config clashes.
345
345
  - Writes can optionally call `bw sync` (internal; not exposed as an MCP tool).
@@ -358,6 +358,12 @@ The server executes `bw` commands on your behalf:
358
358
 
359
359
  There is **no built-in auth** layer in v1. Run it only on a trusted network boundary (localhost, private subnet, VPN, etc.).
360
360
 
361
+ Credential resolution:
362
+
363
+ - **HTTP mode** requires `X-BW-*` headers on every request by default. Without them, tools return an error.
364
+ - **Stdio mode** reads `BW_*` env vars at startup (single-tenant).
365
+ - To allow HTTP mode to fall back to server env vars when headers are absent (single-tenant HTTP), set `KEYCHAIN_ALLOW_ENV_FALLBACK=true`. **Security warning:** this means any client that can reach the HTTP endpoint gets full vault access without providing credentials. Only use this behind network-level access control.
366
+
361
367
  Mutation control:
362
368
 
363
369
  - Set `READONLY=true` to block all write operations (create/edit/delete/move/restore/attachments).
@@ -367,6 +373,7 @@ Mutation control:
367
373
  - `KEYCHAIN_SESSION_SWEEP_INTERVAL_MS` (default `60000`)
368
374
  - `KEYCHAIN_MAX_HEAP_USED_MB` (default `1536`, set `0` to disable memory fuse)
369
375
  - `KEYCHAIN_METRICS_LOG_INTERVAL_MS` (default `0`, disabled)
376
+ - `KEYCHAIN_ALLOW_ENV_FALLBACK` (default `false`; HTTP env-var credential fallback)
370
377
 
371
378
  Redaction defaults (item reads):
372
379
 
@@ -71,13 +71,21 @@ export function bwEnvFromExpressHeaders(req) {
71
71
  }
72
72
  /**
73
73
  * Resolve BwEnv from Express request headers (X-BW-*) first.
74
- * Falls back to environment variables (BW_HOST, BW_PASSWORD, etc.) if no
75
- * BW headers are present. Returns null if neither source provides credentials.
74
+ * When `allowEnvFallback` is true, falls back to environment variables
75
+ * (BW_HOST, BW_PASSWORD, etc.) if no BW headers are present.
76
+ * Returns null if no credentials can be resolved.
77
+ *
78
+ * **Security note:** enabling `allowEnvFallback` in HTTP mode means any
79
+ * client that omits X-BW-* headers will inherit the server's own vault
80
+ * credentials. Only enable this in single-tenant deployments behind
81
+ * network-level access control.
76
82
  */
77
- export function bwEnvFromHeadersOrEnv(req) {
83
+ export function bwEnvFromHeadersOrEnv(req, opts = {}) {
78
84
  const fromHeaders = bwEnvFromExpressHeaders(req);
79
85
  if (fromHeaders)
80
86
  return fromHeaders;
87
+ if (!opts.allowEnvFallback)
88
+ return null;
81
89
  try {
82
90
  return readBwEnv();
83
91
  }
@@ -60,7 +60,13 @@ export class BwSessionManager {
60
60
  env: this.baseEnv(),
61
61
  timeoutMs: 60_000,
62
62
  });
63
- const parsed = JSON.parse(stdout);
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(stdout);
66
+ }
67
+ catch (err) {
68
+ throw new Error(`Failed to parse bw template output: ${stdout.slice(0, 200)}`, { cause: err });
69
+ }
64
70
  this.templateItem = parsed;
65
71
  return parsed;
66
72
  }
@@ -102,7 +108,13 @@ export class BwSessionManager {
102
108
  env: this.baseEnv(),
103
109
  timeoutMs: 60_000,
104
110
  });
105
- const parsed = JSON.parse(stdout);
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(stdout);
114
+ }
115
+ catch (err) {
116
+ throw new Error(`Failed to parse bw status output: ${stdout.slice(0, 200)}`, { cause: err });
117
+ }
106
118
  const serverUrl = typeof parsed.serverUrl === 'string' ? parsed.serverUrl : this.env.host;
107
119
  const userEmail = typeof parsed.userEmail === 'string'
108
120
  ? parsed.userEmail
@@ -0,0 +1,4 @@
1
+ // src/sdk/clone.ts
2
+ export function deepClone(obj) {
3
+ return JSON.parse(JSON.stringify(obj));
4
+ }
@@ -3,10 +3,11 @@ import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, join } from 'node:path';
5
5
  import { BwCliError } from '../bw/bwCli.js';
6
+ import { deepClone } from './clone.js';
6
7
  import { buildBwGenerateArgs } from './generateArgs.js';
7
8
  import { applyItemPatch } from './patch.js';
8
9
  import { redactItem } from './redact.js';
9
- import { generateUsername } from './usernameGenerator.js';
10
+ import { generateUsername, } from './usernameGenerator.js';
10
11
  const ITEM_TYPE = {
11
12
  login: 1,
12
13
  note: 2,
@@ -29,9 +30,6 @@ const URI_MATCH_REVERSE = {
29
30
  4: 'regex',
30
31
  5: 'never',
31
32
  };
32
- function deepClone(obj) {
33
- return JSON.parse(JSON.stringify(obj));
34
- }
35
33
  function encodeJsonForBw(value) {
36
34
  return Buffer.from(JSON.stringify(value), 'utf8').toString('base64');
37
35
  }
@@ -127,7 +125,7 @@ export class KeychainSdk {
127
125
  item.collectionIds = input.collectionIds;
128
126
  const encoded = encodeJsonForBw(item);
129
127
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
130
- const created = JSON.parse(stdout);
128
+ const created = this.parseBwJson(stdout);
131
129
  if (input.attachments?.length) {
132
130
  const dir = await mkdtemp(join(tmpdir(), 'keychain-attach-'));
133
131
  try {
@@ -173,6 +171,14 @@ export class KeychainSdk {
173
171
  valueResult(value, revealed) {
174
172
  return { value, revealed };
175
173
  }
174
+ parseBwJson(stdout) {
175
+ try {
176
+ return JSON.parse(stdout);
177
+ }
178
+ catch (err) {
179
+ throw new Error(`Failed to parse bw CLI output: ${stdout.slice(0, 200)}`, { cause: err });
180
+ }
181
+ }
176
182
  tryParseJson(stdout) {
177
183
  const trimmed = stdout.trim();
178
184
  if (!trimmed)
@@ -328,7 +334,7 @@ export class KeychainSdk {
328
334
  if (input.type === 'text') {
329
335
  if (typeof input.text !== 'string')
330
336
  throw new Error('Missing text for text send');
331
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, input.text], { timeoutMs: 60_000 });
337
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--', input.text], { timeoutMs: 60_000 });
332
338
  return this.tryParseJson(stdout);
333
339
  }
334
340
  if (typeof input.filename !== 'string' ||
@@ -339,7 +345,7 @@ export class KeychainSdk {
339
345
  const filePath = join(dir, basename(input.filename));
340
346
  try {
341
347
  await writeFile(filePath, Buffer.from(input.contentBase64, 'base64'));
342
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, filePath], { timeoutMs: 120_000 });
348
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--', filePath], { timeoutMs: 120_000 });
343
349
  return this.tryParseJson(stdout);
344
350
  }
345
351
  finally {
@@ -372,8 +378,9 @@ export class KeychainSdk {
372
378
  await writeFile(filePath, Buffer.from(input.file.contentBase64, 'base64'));
373
379
  args.push('--file', filePath);
374
380
  }
375
- if (typeof encodedJson === 'string')
376
- args.push(encodedJson);
381
+ if (typeof encodedJson === 'string') {
382
+ args.push('--', encodedJson);
383
+ }
377
384
  const { stdout } = await this.bw.runForSession(session, args, {
378
385
  timeoutMs: 120_000,
379
386
  });
@@ -398,7 +405,7 @@ export class KeychainSdk {
398
405
  const args = ['send', 'edit'];
399
406
  if (typeof input.itemId === 'string')
400
407
  args.push('--itemid', input.itemId);
401
- args.push(encodedJson);
408
+ args.push('--', encodedJson);
402
409
  const { stdout } = await this.bw.runForSession(session, args, {
403
410
  timeoutMs: 120_000,
404
411
  });
@@ -407,20 +414,18 @@ export class KeychainSdk {
407
414
  }
408
415
  async receive(input) {
409
416
  return this.bw.withSession(async (session) => {
410
- const args = ['receive', input.url];
417
+ const opts = ['receive'];
411
418
  if (typeof input.password === 'string')
412
- args.push('--password', input.password);
419
+ opts.push('--password', input.password);
413
420
  if (input.obj) {
414
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--obj'], { timeoutMs: 60_000 });
421
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...opts, '--obj', '--', input.url], { timeoutMs: 60_000 });
415
422
  return this.tryParseJson(stdout);
416
423
  }
417
424
  if (input.downloadFile) {
418
425
  const dir = await mkdtemp(join(tmpdir(), 'keychain-receive-'));
419
426
  const outPath = join(dir, 'received');
420
427
  try {
421
- await this.bw.runForSession(session, [...args, '--output', outPath], {
422
- timeoutMs: 120_000,
423
- });
428
+ await this.bw.runForSession(session, [...opts, '--output', outPath, '--', input.url], { timeoutMs: 120_000 });
424
429
  const buf = await readFile(outPath);
425
430
  return {
426
431
  file: {
@@ -434,7 +439,7 @@ export class KeychainSdk {
434
439
  await rm(dir, { recursive: true, force: true });
435
440
  }
436
441
  }
437
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args], {
442
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...opts, '--', input.url], {
438
443
  timeoutMs: 60_000,
439
444
  });
440
445
  return { text: stdout.trim() };
@@ -483,7 +488,7 @@ export class KeychainSdk {
483
488
  const { stdout } = await this.bw.runForSession(session, args, {
484
489
  timeoutMs: 120_000,
485
490
  });
486
- const results = JSON.parse(stdout);
491
+ const results = this.parseBwJson(stdout);
487
492
  for (const raw of results) {
488
493
  if (!raw || typeof raw !== 'object')
489
494
  continue;
@@ -547,7 +552,7 @@ export class KeychainSdk {
547
552
  const { stdout } = await this.bw.runForSession(session, args, {
548
553
  timeoutMs: 60_000,
549
554
  });
550
- return JSON.parse(stdout);
555
+ return this.parseBwJson(stdout);
551
556
  });
552
557
  return typeof limit === 'number' ? folders.slice(0, limit) : folders;
553
558
  }
@@ -562,7 +567,7 @@ export class KeychainSdk {
562
567
  const { stdout } = await this.bw.runForSession(session, args, {
563
568
  timeoutMs: 60_000,
564
569
  });
565
- return JSON.parse(stdout);
570
+ return this.parseBwJson(stdout);
566
571
  });
567
572
  return typeof limit === 'number'
568
573
  ? collections.slice(0, limit)
@@ -577,20 +582,20 @@ export class KeychainSdk {
577
582
  const { stdout } = await this.bw.runForSession(session, args, {
578
583
  timeoutMs: 60_000,
579
584
  });
580
- return JSON.parse(stdout);
585
+ return this.parseBwJson(stdout);
581
586
  });
582
587
  return typeof limit === 'number' ? orgs.slice(0, limit) : orgs;
583
588
  }
584
- async createFolder(name) {
589
+ async createFolder(input) {
585
590
  return this.bw.withSession(async (session) => {
586
591
  if (this.syncOnWrite()) {
587
592
  await this.bw
588
593
  .runForSession(session, ['sync'], { timeoutMs: 120_000 })
589
594
  .catch(() => { });
590
595
  }
591
- const encoded = encodeJsonForBw({ name });
596
+ const encoded = encodeJsonForBw({ name: input.name });
592
597
  const { stdout } = await this.bw.runForSession(session, ['create', 'folder', encoded], { timeoutMs: 60_000 });
593
- return JSON.parse(stdout);
598
+ return this.parseBwJson(stdout);
594
599
  });
595
600
  }
596
601
  async editFolder(input) {
@@ -602,7 +607,7 @@ export class KeychainSdk {
602
607
  }
603
608
  const encoded = encodeJsonForBw({ name: input.name });
604
609
  const { stdout } = await this.bw.runForSession(session, ['edit', 'folder', input.id, encoded], { timeoutMs: 60_000 });
605
- return JSON.parse(stdout);
610
+ return this.parseBwJson(stdout);
606
611
  });
607
612
  }
608
613
  async deleteFolder(input) {
@@ -631,7 +636,7 @@ export class KeychainSdk {
631
636
  const { stdout } = await this.bw.runForSession(session, args, {
632
637
  timeoutMs: 60_000,
633
638
  });
634
- return JSON.parse(stdout);
639
+ return this.parseBwJson(stdout);
635
640
  });
636
641
  return typeof limit === 'number' ? cols.slice(0, limit) : cols;
637
642
  }
@@ -654,7 +659,7 @@ export class KeychainSdk {
654
659
  input.organizationId,
655
660
  encoded,
656
661
  ], { timeoutMs: 60_000 });
657
- return JSON.parse(stdout);
662
+ return this.parseBwJson(stdout);
658
663
  });
659
664
  }
660
665
  async editOrgCollection(input) {
@@ -678,7 +683,7 @@ export class KeychainSdk {
678
683
  '--organizationid',
679
684
  input.organizationId,
680
685
  ], { timeoutMs: 60_000 });
681
- return JSON.parse(stdout);
686
+ return this.parseBwJson(stdout);
682
687
  });
683
688
  }
684
689
  async deleteOrgCollection(input) {
@@ -711,14 +716,14 @@ export class KeychainSdk {
711
716
  const { stdout } = await this.bw.runForSession(session, args, {
712
717
  timeoutMs: 120_000,
713
718
  });
714
- const moved = JSON.parse(stdout);
719
+ const moved = this.parseBwJson(stdout);
715
720
  return this.maybeRedact(moved, input.reveal);
716
721
  });
717
722
  }
718
723
  async getItem(id, opts = {}) {
719
724
  const item = await this.bw.withSession(async (session) => {
720
725
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
721
- return JSON.parse(stdout);
726
+ return this.parseBwJson(stdout);
722
727
  });
723
728
  return this.maybeRedact(item, opts.reveal);
724
729
  }
@@ -817,12 +822,14 @@ export class KeychainSdk {
817
822
  return this.maybeRedact(JSON.parse(gotOut), input.reveal);
818
823
  });
819
824
  }
825
+ /** Always reveals — username is not considered a secret by Bitwarden. */
820
826
  async getUsername(input) {
821
827
  return this.bw.withSession(async (session) => {
822
828
  const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'username', input.term], { timeoutMs: 60_000 });
823
829
  return this.valueResult(stdout.trim(), true);
824
830
  });
825
831
  }
832
+ /** Requires opts.reveal=true; returns {value: null, revealed: false} when ungated. */
826
833
  async getPassword(input, opts = {}) {
827
834
  if (!opts.reveal)
828
835
  return this.valueResult(null, false);
@@ -839,6 +846,7 @@ export class KeychainSdk {
839
846
  return this.valueResult(stdout.trim(), true);
840
847
  });
841
848
  }
849
+ /** Always reveals — URIs are not considered secrets by Bitwarden. */
842
850
  async getUri(input) {
843
851
  return this.bw.withSession(async (session) => {
844
852
  const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'uri', input.term], { timeoutMs: 60_000 });
@@ -853,6 +861,7 @@ export class KeychainSdk {
853
861
  return this.valueResult(stdout.trim(), true);
854
862
  });
855
863
  }
864
+ /** Always reveals — exposure count is public information from haveibeenpwned. */
856
865
  async getExposed(input) {
857
866
  const isNotFoundError = (err) => {
858
867
  const combined = `${err.stderr}\n${err.stdout}`.trim().toLowerCase();
@@ -888,7 +897,7 @@ export class KeychainSdk {
888
897
  async getFolder(input) {
889
898
  return this.bw.withSession(async (session) => {
890
899
  const { stdout } = await this.bw.runForSession(session, ['get', 'folder', input.id], { timeoutMs: 60_000 });
891
- return JSON.parse(stdout);
900
+ return this.parseBwJson(stdout);
892
901
  });
893
902
  }
894
903
  async getCollection(input) {
@@ -899,13 +908,13 @@ export class KeychainSdk {
899
908
  const { stdout } = await this.bw.runForSession(session, args, {
900
909
  timeoutMs: 60_000,
901
910
  });
902
- return JSON.parse(stdout);
911
+ return this.parseBwJson(stdout);
903
912
  });
904
913
  }
905
914
  async getOrganization(input) {
906
915
  return this.bw.withSession(async (session) => {
907
916
  const { stdout } = await this.bw.runForSession(session, ['get', 'organization', input.id], { timeoutMs: 60_000 });
908
- return JSON.parse(stdout);
917
+ return this.parseBwJson(stdout);
909
918
  });
910
919
  }
911
920
  async getOrgCollection(input) {
@@ -916,13 +925,13 @@ export class KeychainSdk {
916
925
  const { stdout } = await this.bw.runForSession(session, args, {
917
926
  timeoutMs: 60_000,
918
927
  });
919
- return JSON.parse(stdout);
928
+ return this.parseBwJson(stdout);
920
929
  });
921
930
  }
922
931
  async getPasswordHistory(id, opts = {}) {
923
932
  const item = await this.bw.withSession(async (session) => {
924
933
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
925
- return JSON.parse(stdout);
934
+ return this.parseBwJson(stdout);
926
935
  });
927
936
  const history = Array.isArray(item.passwordHistory)
928
937
  ? item.passwordHistory
@@ -999,7 +1008,7 @@ export class KeychainSdk {
999
1008
  item.collectionIds = input.collectionIds;
1000
1009
  const encoded = encodeJsonForBw(item);
1001
1010
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1002
- const created = JSON.parse(stdout);
1011
+ const created = this.parseBwJson(stdout);
1003
1012
  if (input.collectionIds?.length) {
1004
1013
  const encodedCols = encodeJsonForBw(input.collectionIds);
1005
1014
  await this.bw
@@ -1071,7 +1080,7 @@ export class KeychainSdk {
1071
1080
  item.collectionIds = input.collectionIds;
1072
1081
  const encoded = encodeJsonForBw(item);
1073
1082
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1074
- const created = JSON.parse(stdout);
1083
+ const created = this.parseBwJson(stdout);
1075
1084
  if (input.collectionIds?.length) {
1076
1085
  const encodedCols = encodeJsonForBw(input.collectionIds);
1077
1086
  await this.bw
@@ -1113,7 +1122,7 @@ export class KeychainSdk {
1113
1122
  item.collectionIds = input.collectionIds;
1114
1123
  const encoded = encodeJsonForBw(item);
1115
1124
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1116
- const created = JSON.parse(stdout);
1125
+ const created = this.parseBwJson(stdout);
1117
1126
  if (input.collectionIds?.length) {
1118
1127
  const encodedCols = encodeJsonForBw(input.collectionIds);
1119
1128
  await this.bw
@@ -1133,17 +1142,9 @@ export class KeychainSdk {
1133
1142
  .catch(() => { });
1134
1143
  }
1135
1144
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
1136
- const current = JSON.parse(stdout);
1137
- // Convert uri match strings to bw enum numbers if needed.
1138
- const normalizedPatch = deepClone(patch);
1139
- if (normalizedPatch.login?.uris) {
1140
- normalizedPatch.login.uris = normalizedPatch.login.uris.map((u) => ({
1141
- uri: u.uri,
1142
- match: u.match,
1143
- }));
1144
- }
1145
- const next = applyItemPatch(current, normalizedPatch);
1146
- // If uris were provided, convert match strings to numbers now.
1145
+ const current = this.parseBwJson(stdout);
1146
+ const next = applyItemPatch(current, deepClone(patch));
1147
+ // If uris were provided, convert match strings to bw enum numbers.
1147
1148
  if (patch.login?.uris) {
1148
1149
  const login = (next.login && typeof next.login === 'object'
1149
1150
  ? next.login
@@ -1172,7 +1173,7 @@ export class KeychainSdk {
1172
1173
  // BwSessionManager can serialize session access; nesting can deadlock.
1173
1174
  const current = await this.bw.withSession(async (session) => {
1174
1175
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', input.id], { timeoutMs: 60_000 });
1175
- return JSON.parse(stdout);
1176
+ return this.parseBwJson(stdout);
1176
1177
  });
1177
1178
  const currentLogin = current.login && typeof current.login === 'object'
1178
1179
  ? current.login
@@ -1,8 +1,6 @@
1
1
  // src/sdk/redact.ts
2
+ import { deepClone } from './clone.js';
2
3
  export const REDACTED = '[REDACTED]';
3
- function deepClone(obj) {
4
- return JSON.parse(JSON.stringify(obj));
5
- }
6
4
  function redactFields(fields) {
7
5
  return fields.map((f) => {
8
6
  if (!f || typeof f !== 'object')
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/server.ts
2
2
  import { parseArgs } from 'node:util';
3
- import { createKeychainApp } from './app.js';
3
+ import { createKeychainApp } from './transports/http.js';
4
4
  import { runStdioTransport } from './transports/stdio.js';
5
5
  const { values } = parseArgs({
6
6
  options: {
@@ -188,7 +188,7 @@ export function registerTools(server, deps) {
188
188
  if (isReadOnly)
189
189
  return readonlyBlocked();
190
190
  const sdk = await deps.getSdk(extra.authInfo);
191
- const folder = await sdk.createFolder(input.name);
191
+ const folder = await sdk.createFolder({ name: input.name });
192
192
  return {
193
193
  structuredContent: { folder },
194
194
  content: [{ type: 'text', text: 'Created.' }],
@@ -1,4 +1,4 @@
1
- // src/app.ts
1
+ // src/transports/http.ts
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -34,6 +34,9 @@ export function createKeychainApp(opts = {}) {
34
34
  })();
35
35
  const metricsLogIntervalMs = opts.metricsLogIntervalMs ??
36
36
  parseNonNegativeInt(process.env.KEYCHAIN_METRICS_LOG_INTERVAL_MS, 0);
37
+ const allowEnvFallback = opts.allowEnvFallback ??
38
+ (process.env.KEYCHAIN_ALLOW_ENV_FALLBACK ?? 'false').toLowerCase() ===
39
+ 'true';
37
40
  const pool = new BwSessionPool({
38
41
  rootDir: opts.bwHomeRoot ??
39
42
  process.env.KEYCHAIN_BW_HOME_ROOT ??
@@ -55,12 +58,13 @@ export function createKeychainApp(opts = {}) {
55
58
  });
56
59
  return server;
57
60
  }
58
- async function withBwHeaders(req) {
59
- const bwEnv = bwEnvFromHeadersOrEnv(req);
61
+ const BW_HEADERS_SENTINEL = 'x-bw-headers';
62
+ function withBwHeaders(req) {
63
+ const bwEnv = bwEnvFromHeadersOrEnv(req, { allowEnvFallback });
60
64
  req.auth = bwEnv
61
65
  ? {
62
- token: 'x-bw-headers',
63
- clientId: 'x-bw-headers',
66
+ token: BW_HEADERS_SENTINEL,
67
+ clientId: BW_HEADERS_SENTINEL,
64
68
  scopes: [],
65
69
  extra: { bwEnv },
66
70
  }
@@ -232,7 +236,7 @@ export function createKeychainApp(opts = {}) {
232
236
  });
233
237
  return;
234
238
  }
235
- await withBwHeaders(req);
239
+ withBwHeaders(req);
236
240
  const sessionIdHeader = req.headers['mcp-session-id'];
237
241
  const sessionId = typeof sessionIdHeader === 'string'
238
242
  ? sessionIdHeader
@@ -316,10 +320,8 @@ export function createKeychainApp(opts = {}) {
316
320
  }
317
321
  return;
318
322
  }
319
- // Initialize a new session
320
- if (!sessionId &&
321
- req.method === 'POST' &&
322
- isInitializeRequest(req.body)) {
323
+ // No session id — start fresh session
324
+ if (req.method === 'POST' && isInitializeRequest(req.body)) {
323
325
  if (rejectIfMemoryFuseTripped(res))
324
326
  return;
325
327
  if (rejectIfSessionCapacityReached(res))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@icoretech/warden-mcp",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "description": "Vaultwarden/Bitwarden MCP server backed by Bitwarden CLI (bw).",
@@ -44,7 +44,6 @@
44
44
  "dependencies": {
45
45
  "@modelcontextprotocol/sdk": "^1.27.1",
46
46
  "express": "^5.2.1",
47
- "jose": "^6.2.2",
48
47
  "zod": "^4.3.6"
49
48
  },
50
49
  "optionalDependencies": {