@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 +8 -1
- package/dist/bw/bwHeaders.js +11 -3
- package/dist/bw/bwSession.js +14 -2
- package/dist/sdk/clone.js +4 -0
- package/dist/sdk/keychainSdk.js +56 -50
- package/dist/sdk/redact.js +1 -3
- package/dist/server.js +1 -1
- package/dist/tools/registerTools.js +1 -1
- package/dist/transports/http.js +12 -10
- package/package.json +1 -2
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
|
|
package/dist/bw/bwHeaders.js
CHANGED
|
@@ -71,13 +71,21 @@ export function bwEnvFromExpressHeaders(req) {
|
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Resolve BwEnv from Express request headers (X-BW-*) first.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
}
|
package/dist/bw/bwSession.js
CHANGED
|
@@ -60,7 +60,13 @@ export class BwSessionManager {
|
|
|
60
60
|
env: this.baseEnv(),
|
|
61
61
|
timeoutMs: 60_000,
|
|
62
62
|
});
|
|
63
|
-
|
|
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
|
-
|
|
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
|
package/dist/sdk/keychainSdk.js
CHANGED
|
@@ -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 =
|
|
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
|
|
422
|
+
const opts = ['receive'];
|
|
411
423
|
if (typeof input.password === 'string')
|
|
412
|
-
|
|
424
|
+
opts.push('--password', input.password);
|
|
413
425
|
if (input.obj) {
|
|
414
|
-
const { stdout } = await this.bw.runForSession(session, ['--raw', ...
|
|
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, [...
|
|
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', ...
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
590
|
+
return this.parseBwJson(stdout);
|
|
581
591
|
});
|
|
582
592
|
return typeof limit === 'number' ? orgs.slice(0, limit) : orgs;
|
|
583
593
|
}
|
|
584
|
-
async createFolder(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
|
1181
|
+
return this.parseBwJson(stdout);
|
|
1176
1182
|
});
|
|
1177
1183
|
const currentLogin = current.login && typeof current.login === 'object'
|
|
1178
1184
|
? current.login
|
package/dist/sdk/redact.js
CHANGED
|
@@ -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 './
|
|
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.' }],
|
package/dist/transports/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
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
|
-
|
|
59
|
-
|
|
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:
|
|
63
|
-
clientId:
|
|
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
|
-
|
|
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
|
-
//
|
|
320
|
-
if (
|
|
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.
|
|
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": {
|