@icoretech/warden-mcp 0.1.6 → 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 +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 +51 -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.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
|
-
|
|
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
|
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,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
|
|
417
|
+
const opts = ['receive'];
|
|
411
418
|
if (typeof input.password === 'string')
|
|
412
|
-
|
|
419
|
+
opts.push('--password', input.password);
|
|
413
420
|
if (input.obj) {
|
|
414
|
-
const { stdout } = await this.bw.runForSession(session, ['--raw', ...
|
|
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, [...
|
|
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', ...
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
585
|
+
return this.parseBwJson(stdout);
|
|
581
586
|
});
|
|
582
587
|
return typeof limit === 'number' ? orgs.slice(0, limit) : orgs;
|
|
583
588
|
}
|
|
584
|
-
async createFolder(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
1176
|
+
return this.parseBwJson(stdout);
|
|
1176
1177
|
});
|
|
1177
1178
|
const currentLogin = current.login && typeof current.login === 'object'
|
|
1178
1179
|
? 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.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": {
|