@icoretech/warden-mcp 0.1.6 → 0.1.8

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.length} bytes)`, { 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.length} bytes)`, { 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,19 @@ 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
+ // Do not include raw stdout — it may contain unredacted secrets.
180
+ const length = stdout.length;
181
+ const preview = stdout.startsWith('{')
182
+ ? '{...}'
183
+ : stdout.slice(0, 8).replace(/[^\x20-\x7E]/g, '?');
184
+ throw new Error(`Failed to parse bw CLI output (${length} bytes, starts with: ${preview})`, { cause: err });
185
+ }
186
+ }
176
187
  tryParseJson(stdout) {
177
188
  const trimmed = stdout.trim();
178
189
  if (!trimmed)
@@ -328,7 +339,7 @@ export class KeychainSdk {
328
339
  if (input.type === 'text') {
329
340
  if (typeof input.text !== 'string')
330
341
  throw new Error('Missing text for text send');
331
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, input.text], { timeoutMs: 60_000 });
342
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--', input.text], { timeoutMs: 60_000 });
332
343
  return this.tryParseJson(stdout);
333
344
  }
334
345
  if (typeof input.filename !== 'string' ||
@@ -339,7 +350,7 @@ export class KeychainSdk {
339
350
  const filePath = join(dir, basename(input.filename));
340
351
  try {
341
352
  await writeFile(filePath, Buffer.from(input.contentBase64, 'base64'));
342
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, filePath], { timeoutMs: 120_000 });
353
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--', filePath], { timeoutMs: 120_000 });
343
354
  return this.tryParseJson(stdout);
344
355
  }
345
356
  finally {
@@ -372,8 +383,9 @@ export class KeychainSdk {
372
383
  await writeFile(filePath, Buffer.from(input.file.contentBase64, 'base64'));
373
384
  args.push('--file', filePath);
374
385
  }
375
- if (typeof encodedJson === 'string')
376
- args.push(encodedJson);
386
+ if (typeof encodedJson === 'string') {
387
+ args.push('--', encodedJson);
388
+ }
377
389
  const { stdout } = await this.bw.runForSession(session, args, {
378
390
  timeoutMs: 120_000,
379
391
  });
@@ -398,7 +410,7 @@ export class KeychainSdk {
398
410
  const args = ['send', 'edit'];
399
411
  if (typeof input.itemId === 'string')
400
412
  args.push('--itemid', input.itemId);
401
- args.push(encodedJson);
413
+ args.push('--', encodedJson);
402
414
  const { stdout } = await this.bw.runForSession(session, args, {
403
415
  timeoutMs: 120_000,
404
416
  });
@@ -407,20 +419,18 @@ export class KeychainSdk {
407
419
  }
408
420
  async receive(input) {
409
421
  return this.bw.withSession(async (session) => {
410
- const args = ['receive', input.url];
422
+ const opts = ['receive'];
411
423
  if (typeof input.password === 'string')
412
- args.push('--password', input.password);
424
+ opts.push('--password', input.password);
413
425
  if (input.obj) {
414
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--obj'], { timeoutMs: 60_000 });
426
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...opts, '--obj', '--', input.url], { timeoutMs: 60_000 });
415
427
  return this.tryParseJson(stdout);
416
428
  }
417
429
  if (input.downloadFile) {
418
430
  const dir = await mkdtemp(join(tmpdir(), 'keychain-receive-'));
419
431
  const outPath = join(dir, 'received');
420
432
  try {
421
- await this.bw.runForSession(session, [...args, '--output', outPath], {
422
- timeoutMs: 120_000,
423
- });
433
+ await this.bw.runForSession(session, [...opts, '--output', outPath, '--', input.url], { timeoutMs: 120_000 });
424
434
  const buf = await readFile(outPath);
425
435
  return {
426
436
  file: {
@@ -434,7 +444,7 @@ export class KeychainSdk {
434
444
  await rm(dir, { recursive: true, force: true });
435
445
  }
436
446
  }
437
- const { stdout } = await this.bw.runForSession(session, ['--raw', ...args], {
447
+ const { stdout } = await this.bw.runForSession(session, ['--raw', ...opts, '--', input.url], {
438
448
  timeoutMs: 60_000,
439
449
  });
440
450
  return { text: stdout.trim() };
@@ -483,7 +493,7 @@ export class KeychainSdk {
483
493
  const { stdout } = await this.bw.runForSession(session, args, {
484
494
  timeoutMs: 120_000,
485
495
  });
486
- const results = JSON.parse(stdout);
496
+ const results = this.parseBwJson(stdout);
487
497
  for (const raw of results) {
488
498
  if (!raw || typeof raw !== 'object')
489
499
  continue;
@@ -547,7 +557,7 @@ export class KeychainSdk {
547
557
  const { stdout } = await this.bw.runForSession(session, args, {
548
558
  timeoutMs: 60_000,
549
559
  });
550
- return JSON.parse(stdout);
560
+ return this.parseBwJson(stdout);
551
561
  });
552
562
  return typeof limit === 'number' ? folders.slice(0, limit) : folders;
553
563
  }
@@ -562,7 +572,7 @@ export class KeychainSdk {
562
572
  const { stdout } = await this.bw.runForSession(session, args, {
563
573
  timeoutMs: 60_000,
564
574
  });
565
- return JSON.parse(stdout);
575
+ return this.parseBwJson(stdout);
566
576
  });
567
577
  return typeof limit === 'number'
568
578
  ? collections.slice(0, limit)
@@ -577,20 +587,20 @@ export class KeychainSdk {
577
587
  const { stdout } = await this.bw.runForSession(session, args, {
578
588
  timeoutMs: 60_000,
579
589
  });
580
- return JSON.parse(stdout);
590
+ return this.parseBwJson(stdout);
581
591
  });
582
592
  return typeof limit === 'number' ? orgs.slice(0, limit) : orgs;
583
593
  }
584
- async createFolder(name) {
594
+ async createFolder(input) {
585
595
  return this.bw.withSession(async (session) => {
586
596
  if (this.syncOnWrite()) {
587
597
  await this.bw
588
598
  .runForSession(session, ['sync'], { timeoutMs: 120_000 })
589
599
  .catch(() => { });
590
600
  }
591
- const encoded = encodeJsonForBw({ name });
601
+ const encoded = encodeJsonForBw({ name: input.name });
592
602
  const { stdout } = await this.bw.runForSession(session, ['create', 'folder', encoded], { timeoutMs: 60_000 });
593
- return JSON.parse(stdout);
603
+ return this.parseBwJson(stdout);
594
604
  });
595
605
  }
596
606
  async editFolder(input) {
@@ -602,7 +612,7 @@ export class KeychainSdk {
602
612
  }
603
613
  const encoded = encodeJsonForBw({ name: input.name });
604
614
  const { stdout } = await this.bw.runForSession(session, ['edit', 'folder', input.id, encoded], { timeoutMs: 60_000 });
605
- return JSON.parse(stdout);
615
+ return this.parseBwJson(stdout);
606
616
  });
607
617
  }
608
618
  async deleteFolder(input) {
@@ -631,7 +641,7 @@ export class KeychainSdk {
631
641
  const { stdout } = await this.bw.runForSession(session, args, {
632
642
  timeoutMs: 60_000,
633
643
  });
634
- return JSON.parse(stdout);
644
+ return this.parseBwJson(stdout);
635
645
  });
636
646
  return typeof limit === 'number' ? cols.slice(0, limit) : cols;
637
647
  }
@@ -654,7 +664,7 @@ export class KeychainSdk {
654
664
  input.organizationId,
655
665
  encoded,
656
666
  ], { timeoutMs: 60_000 });
657
- return JSON.parse(stdout);
667
+ return this.parseBwJson(stdout);
658
668
  });
659
669
  }
660
670
  async editOrgCollection(input) {
@@ -678,7 +688,7 @@ export class KeychainSdk {
678
688
  '--organizationid',
679
689
  input.organizationId,
680
690
  ], { timeoutMs: 60_000 });
681
- return JSON.parse(stdout);
691
+ return this.parseBwJson(stdout);
682
692
  });
683
693
  }
684
694
  async deleteOrgCollection(input) {
@@ -711,14 +721,14 @@ export class KeychainSdk {
711
721
  const { stdout } = await this.bw.runForSession(session, args, {
712
722
  timeoutMs: 120_000,
713
723
  });
714
- const moved = JSON.parse(stdout);
724
+ const moved = this.parseBwJson(stdout);
715
725
  return this.maybeRedact(moved, input.reveal);
716
726
  });
717
727
  }
718
728
  async getItem(id, opts = {}) {
719
729
  const item = await this.bw.withSession(async (session) => {
720
730
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
721
- return JSON.parse(stdout);
731
+ return this.parseBwJson(stdout);
722
732
  });
723
733
  return this.maybeRedact(item, opts.reveal);
724
734
  }
@@ -817,12 +827,14 @@ export class KeychainSdk {
817
827
  return this.maybeRedact(JSON.parse(gotOut), input.reveal);
818
828
  });
819
829
  }
830
+ /** Always reveals — username is not considered a secret by Bitwarden. */
820
831
  async getUsername(input) {
821
832
  return this.bw.withSession(async (session) => {
822
833
  const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'username', input.term], { timeoutMs: 60_000 });
823
834
  return this.valueResult(stdout.trim(), true);
824
835
  });
825
836
  }
837
+ /** Requires opts.reveal=true; returns {value: null, revealed: false} when ungated. */
826
838
  async getPassword(input, opts = {}) {
827
839
  if (!opts.reveal)
828
840
  return this.valueResult(null, false);
@@ -839,6 +851,7 @@ export class KeychainSdk {
839
851
  return this.valueResult(stdout.trim(), true);
840
852
  });
841
853
  }
854
+ /** Always reveals — URIs are not considered secrets by Bitwarden. */
842
855
  async getUri(input) {
843
856
  return this.bw.withSession(async (session) => {
844
857
  const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'uri', input.term], { timeoutMs: 60_000 });
@@ -853,6 +866,7 @@ export class KeychainSdk {
853
866
  return this.valueResult(stdout.trim(), true);
854
867
  });
855
868
  }
869
+ /** Always reveals — exposure count is public information from haveibeenpwned. */
856
870
  async getExposed(input) {
857
871
  const isNotFoundError = (err) => {
858
872
  const combined = `${err.stderr}\n${err.stdout}`.trim().toLowerCase();
@@ -888,7 +902,7 @@ export class KeychainSdk {
888
902
  async getFolder(input) {
889
903
  return this.bw.withSession(async (session) => {
890
904
  const { stdout } = await this.bw.runForSession(session, ['get', 'folder', input.id], { timeoutMs: 60_000 });
891
- return JSON.parse(stdout);
905
+ return this.parseBwJson(stdout);
892
906
  });
893
907
  }
894
908
  async getCollection(input) {
@@ -899,13 +913,13 @@ export class KeychainSdk {
899
913
  const { stdout } = await this.bw.runForSession(session, args, {
900
914
  timeoutMs: 60_000,
901
915
  });
902
- return JSON.parse(stdout);
916
+ return this.parseBwJson(stdout);
903
917
  });
904
918
  }
905
919
  async getOrganization(input) {
906
920
  return this.bw.withSession(async (session) => {
907
921
  const { stdout } = await this.bw.runForSession(session, ['get', 'organization', input.id], { timeoutMs: 60_000 });
908
- return JSON.parse(stdout);
922
+ return this.parseBwJson(stdout);
909
923
  });
910
924
  }
911
925
  async getOrgCollection(input) {
@@ -916,13 +930,13 @@ export class KeychainSdk {
916
930
  const { stdout } = await this.bw.runForSession(session, args, {
917
931
  timeoutMs: 60_000,
918
932
  });
919
- return JSON.parse(stdout);
933
+ return this.parseBwJson(stdout);
920
934
  });
921
935
  }
922
936
  async getPasswordHistory(id, opts = {}) {
923
937
  const item = await this.bw.withSession(async (session) => {
924
938
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
925
- return JSON.parse(stdout);
939
+ return this.parseBwJson(stdout);
926
940
  });
927
941
  const history = Array.isArray(item.passwordHistory)
928
942
  ? item.passwordHistory
@@ -999,7 +1013,7 @@ export class KeychainSdk {
999
1013
  item.collectionIds = input.collectionIds;
1000
1014
  const encoded = encodeJsonForBw(item);
1001
1015
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1002
- const created = JSON.parse(stdout);
1016
+ const created = this.parseBwJson(stdout);
1003
1017
  if (input.collectionIds?.length) {
1004
1018
  const encodedCols = encodeJsonForBw(input.collectionIds);
1005
1019
  await this.bw
@@ -1071,7 +1085,7 @@ export class KeychainSdk {
1071
1085
  item.collectionIds = input.collectionIds;
1072
1086
  const encoded = encodeJsonForBw(item);
1073
1087
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1074
- const created = JSON.parse(stdout);
1088
+ const created = this.parseBwJson(stdout);
1075
1089
  if (input.collectionIds?.length) {
1076
1090
  const encodedCols = encodeJsonForBw(input.collectionIds);
1077
1091
  await this.bw
@@ -1113,7 +1127,7 @@ export class KeychainSdk {
1113
1127
  item.collectionIds = input.collectionIds;
1114
1128
  const encoded = encodeJsonForBw(item);
1115
1129
  const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
1116
- const created = JSON.parse(stdout);
1130
+ const created = this.parseBwJson(stdout);
1117
1131
  if (input.collectionIds?.length) {
1118
1132
  const encodedCols = encodeJsonForBw(input.collectionIds);
1119
1133
  await this.bw
@@ -1133,17 +1147,9 @@ export class KeychainSdk {
1133
1147
  .catch(() => { });
1134
1148
  }
1135
1149
  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.
1150
+ const current = this.parseBwJson(stdout);
1151
+ const next = applyItemPatch(current, deepClone(patch));
1152
+ // If uris were provided, convert match strings to bw enum numbers.
1147
1153
  if (patch.login?.uris) {
1148
1154
  const login = (next.login && typeof next.login === 'object'
1149
1155
  ? next.login
@@ -1172,7 +1178,7 @@ export class KeychainSdk {
1172
1178
  // BwSessionManager can serialize session access; nesting can deadlock.
1173
1179
  const current = await this.bw.withSession(async (session) => {
1174
1180
  const { stdout } = await this.bw.runForSession(session, ['get', 'item', input.id], { timeoutMs: 60_000 });
1175
- return JSON.parse(stdout);
1181
+ return this.parseBwJson(stdout);
1176
1182
  });
1177
1183
  const currentLogin = current.login && typeof current.login === 'object'
1178
1184
  ? 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.6",
4
+ "version": "0.1.8",
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": {