@icoretech/warden-mcp 0.1.16 → 0.1.18
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 +9 -6
- package/bin/patch-bitwarden-cli.js +31 -0
- package/bin/warden-mcp.js +9 -38
- package/dist/bw/bwCli.js +10 -2
- package/dist/bw/bwSession.js +57 -17
- package/dist/bw/resolveBwBin.js +14 -0
- package/dist/sdk/keychainSdk.js +91 -5
- package/dist/startup/bwStartup.js +24 -0
- package/dist/tools/registerTools.js +6 -3
- package/package.json +5 -2
- package/patches/@bitwarden+cli+2026.2.0.patch +80 -0
package/README.md
CHANGED
|
@@ -62,12 +62,14 @@ BW_BIN=/absolute/path/to/bw npx -y @icoretech/warden-mcp@latest
|
|
|
62
62
|
`warden-mcp` intentionally bundles a vetted `@bitwarden/cli` version instead of
|
|
63
63
|
blindly following the newest upstream CLI on every release. New `bw` releases
|
|
64
64
|
can change login and unlock behavior in ways that break automation, so `bw`
|
|
65
|
-
upgrades should be smoke-tested against real Vaultwarden
|
|
66
|
-
|
|
65
|
+
upgrades should be smoke-tested against real Vaultwarden flows before bumping
|
|
66
|
+
the bundled version. Official Bitwarden compatibility is intended, but it is
|
|
67
|
+
not continuously proven in CI without a real Bitwarden tenant.
|
|
67
68
|
|
|
68
|
-
This repository's compose smoke now exercises both
|
|
69
|
-
user API-key auth against a
|
|
70
|
-
not rely on unit coverage
|
|
69
|
+
This repository's compose smoke now exercises both direct `bw` auth flows and
|
|
70
|
+
the MCP/SDK layers with username/password auth plus user API-key auth against a
|
|
71
|
+
real local Vaultwarden, so `@bitwarden/cli` bumps do not rely on unit coverage
|
|
72
|
+
alone.
|
|
71
73
|
|
|
72
74
|
## Install And Run
|
|
73
75
|
|
|
@@ -456,7 +458,8 @@ Run integration tests:
|
|
|
456
458
|
make test
|
|
457
459
|
```
|
|
458
460
|
|
|
459
|
-
`make test` now runs both compose-backed auth paths
|
|
461
|
+
`make test` now runs both compose-backed auth paths and verifies them at the
|
|
462
|
+
raw CLI plus MCP/SDK layers:
|
|
460
463
|
|
|
461
464
|
- user/password login from `.env.test`
|
|
462
465
|
- api-key login from `tmp/vaultwarden-bootstrap/apikey.env`, generated by the bootstrap step and kept out of git via `tmp/`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const rootDir = resolve(__dirname, '..');
|
|
12
|
+
const cliPackageDir = resolve(__dirname, '../node_modules/@bitwarden/cli');
|
|
13
|
+
|
|
14
|
+
if (!existsSync(cliPackageDir)) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const patchPackageEntrypoint = require.resolve('patch-package/dist/index.js');
|
|
19
|
+
const result = spawnSync(process.execPath, [patchPackageEntrypoint], {
|
|
20
|
+
cwd: rootDir,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
console.error(
|
|
26
|
+
`[warden-mcp] failed to execute patch-package: ${result.error.message}`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.exit(result.status ?? 1);
|
package/bin/warden-mcp.js
CHANGED
|
@@ -2,50 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/warden-mcp.js — CLI entry for @icoretech/warden-mcp
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { accessSync, constants, existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
8
6
|
import { dirname, resolve } from 'node:path';
|
|
9
7
|
import { fileURLToPath } from 'node:url';
|
|
10
8
|
|
|
11
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
if (!
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
const pkgManifest = require.resolve('@bitwarden/cli/package.json');
|
|
21
|
-
const pkgJson = JSON.parse(readFileSync(pkgManifest, 'utf8'));
|
|
22
|
-
const candidate = resolveBundledBwCandidate(pkgManifest, pkgJson.bin);
|
|
23
|
-
if (existsSync(candidate)) {
|
|
24
|
-
try {
|
|
25
|
-
accessSync(candidate, constants.X_OK);
|
|
26
|
-
process.env.BW_BIN = candidate;
|
|
27
|
-
} catch {
|
|
28
|
-
// Not executable — fall through to system bw
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// @bitwarden/cli optional dep not installed — fall through to system bw
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Verify bw is available (either from optional dep or system PATH)
|
|
37
|
-
if (!process.env.BW_BIN) {
|
|
38
|
-
const probe = spawnSync('bw', ['--version'], { encoding: 'utf8' });
|
|
39
|
-
if (probe.error) {
|
|
40
|
-
console.error(
|
|
41
|
-
'[warden-mcp] ERROR: bw CLI not found.\n' +
|
|
42
|
-
'Install it with: npm install -g @bitwarden/cli\n' +
|
|
43
|
-
'Or set the BW_BIN environment variable to the path of the bw binary.',
|
|
44
|
-
);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
// System bw is available — bwCli.ts will find it via PATH
|
|
11
|
+
const startupPath = resolve(__dirname, '../dist/startup/bwStartup.js');
|
|
12
|
+
if (!existsSync(startupPath)) {
|
|
13
|
+
console.error(
|
|
14
|
+
'[warden-mcp] ERROR: dist/startup/bwStartup.js not found. Run `npm run build` first.',
|
|
15
|
+
);
|
|
16
|
+
process.exit(1);
|
|
48
17
|
}
|
|
18
|
+
const { prepareBwStartup } = await import(startupPath);
|
|
19
|
+
prepareBwStartup(process.env);
|
|
49
20
|
|
|
50
21
|
// Delegate to the compiled server entry, forwarding all arguments.
|
|
51
22
|
const serverPath = resolve(__dirname, '../dist/server.js');
|
package/dist/bw/bwCli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/bw/bwCli.ts
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { resolveBundledBwBin } from './resolveBwBin.js';
|
|
3
4
|
export class BwCliError extends Error {
|
|
4
5
|
exitCode;
|
|
5
6
|
stdout;
|
|
@@ -13,7 +14,7 @@ export class BwCliError extends Error {
|
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
export async function runBw(args, opts = {}) {
|
|
16
|
-
const bwBin = process.env.BW_BIN ?? 'bw';
|
|
17
|
+
const bwBin = process.env.BW_BIN ?? resolveBundledBwBin() ?? 'bw';
|
|
17
18
|
// Ensure the CLI never blocks waiting for a prompt (e.g. master password).
|
|
18
19
|
// This is critical for running as an MCP server / in test automation.
|
|
19
20
|
const injectNoInteraction = opts.noInteraction ?? true;
|
|
@@ -80,7 +81,14 @@ export async function runBw(args, opts = {}) {
|
|
|
80
81
|
}, timeoutMs);
|
|
81
82
|
});
|
|
82
83
|
const completed = new Promise((resolve, reject) => {
|
|
83
|
-
child.on('error',
|
|
84
|
+
child.on('error', (error) => {
|
|
85
|
+
const safeCmd = `${bwBin} ${safeRenderedArgs(finalArgs)}`;
|
|
86
|
+
if (error.code === 'ENOENT') {
|
|
87
|
+
reject(new Error(`bw CLI not available for ${safeCmd}. Install @bitwarden/cli or set BW_BIN to a valid bw binary.`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
reject(new Error(`Failed to start ${safeCmd}: ${error.message}`));
|
|
91
|
+
});
|
|
84
92
|
child.on('close', (code) => {
|
|
85
93
|
if (timeout)
|
|
86
94
|
clearTimeout(timeout);
|
package/dist/bw/bwSession.js
CHANGED
|
@@ -3,6 +3,11 @@ import { rm } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { runBw } from './bwCli.js';
|
|
5
5
|
import { Mutex } from './mutex.js';
|
|
6
|
+
const POST_LOGIN_UNLOCK_RETRY_ATTEMPTS = 20;
|
|
7
|
+
const POST_LOGIN_UNLOCK_RETRY_DELAY_MS = 2_000;
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
6
11
|
function requiredEnv(name) {
|
|
7
12
|
const v = process.env[name];
|
|
8
13
|
if (!v) {
|
|
@@ -145,6 +150,19 @@ export class BwSessionManager {
|
|
|
145
150
|
env: this.baseEnv(opts.env),
|
|
146
151
|
});
|
|
147
152
|
}
|
|
153
|
+
async resetCliProfile() {
|
|
154
|
+
this.session = null;
|
|
155
|
+
this.templateItem = null;
|
|
156
|
+
this.configuredHost = null;
|
|
157
|
+
await runBw(['logout'], { env: this.baseEnv(), timeoutMs: 30_000 }).catch(() => { });
|
|
158
|
+
const home = this.homeDir;
|
|
159
|
+
await rm(join(home, '.config', 'Bitwarden CLI', 'data.json'), {
|
|
160
|
+
force: true,
|
|
161
|
+
}).catch(() => { });
|
|
162
|
+
await rm(join(home, '.config', 'Bitwarden CLI', 'config.json'), {
|
|
163
|
+
force: true,
|
|
164
|
+
}).catch(() => { });
|
|
165
|
+
}
|
|
148
166
|
async ensureUnlockedInternal() {
|
|
149
167
|
// Ensure server config points to BW_HOST.
|
|
150
168
|
await this.ensureServerConfigured();
|
|
@@ -188,7 +206,7 @@ export class BwSessionManager {
|
|
|
188
206
|
timeoutMs: 60_000,
|
|
189
207
|
noInteraction: false,
|
|
190
208
|
});
|
|
191
|
-
return stdout.trim();
|
|
209
|
+
return { completed: true, session: stdout.trim() };
|
|
192
210
|
}
|
|
193
211
|
const { stdout } = await runBw([
|
|
194
212
|
'login',
|
|
@@ -197,19 +215,47 @@ export class BwSessionManager {
|
|
|
197
215
|
'BW_PASSWORD',
|
|
198
216
|
'--raw',
|
|
199
217
|
], { env: unlockEnv, timeoutMs: 60_000, noInteraction: false });
|
|
200
|
-
return stdout.trim();
|
|
218
|
+
return { completed: true, session: stdout.trim() };
|
|
201
219
|
}
|
|
202
220
|
catch {
|
|
203
|
-
return '';
|
|
221
|
+
return { completed: false, session: '' };
|
|
204
222
|
}
|
|
205
223
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
const retryUnlockAfterLogin = async () => {
|
|
225
|
+
for (let attempt = 0; attempt < POST_LOGIN_UNLOCK_RETRY_ATTEMPTS; attempt += 1) {
|
|
226
|
+
const session = await tryUnlock();
|
|
227
|
+
if (session)
|
|
228
|
+
return session;
|
|
229
|
+
if (attempt < POST_LOGIN_UNLOCK_RETRY_ATTEMPTS - 1) {
|
|
230
|
+
await sleep(POST_LOGIN_UNLOCK_RETRY_DELAY_MS);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return '';
|
|
234
|
+
};
|
|
235
|
+
const obtainSession = async () => {
|
|
236
|
+
// Prefer unlocking first (works when already logged in). If it yields an
|
|
237
|
+
// empty stdout on exit=0 (observed in some bw builds), fall back to
|
|
238
|
+
// login --raw.
|
|
239
|
+
let session = await tryUnlock();
|
|
240
|
+
if (!session) {
|
|
241
|
+
const login = await tryLoginRaw();
|
|
242
|
+
if (login.session) {
|
|
243
|
+
session = login.session;
|
|
244
|
+
}
|
|
245
|
+
else if (login.completed) {
|
|
246
|
+
session = await retryUnlockAfterLogin();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!session)
|
|
250
|
+
session = await tryUnlock();
|
|
251
|
+
return session;
|
|
252
|
+
};
|
|
253
|
+
let session = await obtainSession();
|
|
254
|
+
if (!session) {
|
|
255
|
+
await this.resetCliProfile();
|
|
256
|
+
await this.ensureServerConfigured();
|
|
257
|
+
session = await obtainSession();
|
|
258
|
+
}
|
|
213
259
|
if (!session)
|
|
214
260
|
throw new Error('bw login/unlock returned an empty session');
|
|
215
261
|
this.session = session;
|
|
@@ -231,13 +277,7 @@ export class BwSessionManager {
|
|
|
231
277
|
catch {
|
|
232
278
|
// If the CLI data is corrupt/out-of-sync, wiping config is the fastest recovery.
|
|
233
279
|
}
|
|
234
|
-
|
|
235
|
-
await rm(join(home, '.config', 'Bitwarden CLI', 'data.json'), {
|
|
236
|
-
force: true,
|
|
237
|
-
}).catch(() => { });
|
|
238
|
-
await rm(join(home, '.config', 'Bitwarden CLI', 'config.json'), {
|
|
239
|
-
force: true,
|
|
240
|
-
}).catch(() => { });
|
|
280
|
+
await this.resetCliProfile();
|
|
241
281
|
await runBw(['config', 'server', this.env.host], {
|
|
242
282
|
env: this.baseEnv(),
|
|
243
283
|
timeoutMs: 30_000,
|
package/dist/bw/resolveBwBin.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
1
3
|
import { dirname, join } from 'node:path';
|
|
2
4
|
export function resolveBundledBwCandidate(pkgManifestPath, pkgBin) {
|
|
3
5
|
const pkgDir = dirname(pkgManifestPath);
|
|
@@ -8,3 +10,15 @@ export function resolveBundledBwCandidate(pkgManifestPath, pkgBin) {
|
|
|
8
10
|
: 'dist/bw';
|
|
9
11
|
return join(pkgDir, binEntry);
|
|
10
12
|
}
|
|
13
|
+
export function resolveBundledBwBin(resolvePackage = createRequire(import.meta.url).resolve) {
|
|
14
|
+
try {
|
|
15
|
+
const pkgManifestPath = resolvePackage('@bitwarden/cli/package.json');
|
|
16
|
+
const pkgJson = JSON.parse(readFileSync(pkgManifestPath, 'utf8'));
|
|
17
|
+
const candidate = resolveBundledBwCandidate(pkgManifestPath, pkgJson.bin);
|
|
18
|
+
accessSync(candidate, constants.X_OK);
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/sdk/keychainSdk.js
CHANGED
|
@@ -168,8 +168,85 @@ export class KeychainSdk {
|
|
|
168
168
|
maybeRedact(value, reveal) {
|
|
169
169
|
return (reveal ? value : redactItem(value));
|
|
170
170
|
}
|
|
171
|
-
valueResult(value, revealed) {
|
|
172
|
-
return { value, revealed };
|
|
171
|
+
valueResult(value, revealed, extra) {
|
|
172
|
+
return { value, revealed, ...(extra ?? {}) };
|
|
173
|
+
}
|
|
174
|
+
extractLoginTotp(item) {
|
|
175
|
+
if (!item || typeof item !== 'object')
|
|
176
|
+
return null;
|
|
177
|
+
const login = item.login;
|
|
178
|
+
if (!login || typeof login !== 'object')
|
|
179
|
+
return null;
|
|
180
|
+
const totp = login.totp;
|
|
181
|
+
return typeof totp === 'string' && totp.trim().length > 0 ? totp : null;
|
|
182
|
+
}
|
|
183
|
+
computeTotpMetadata(rawTotp, nowMs = Date.now()) {
|
|
184
|
+
if (!rawTotp)
|
|
185
|
+
return { period: null, timeLeft: null };
|
|
186
|
+
let period = 30;
|
|
187
|
+
if (rawTotp.startsWith('otpauth://')) {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = new URL(rawTotp);
|
|
190
|
+
const candidate = Number.parseInt(parsed.searchParams.get('period') ?? '', 10);
|
|
191
|
+
if (Number.isFinite(candidate) && candidate > 0) {
|
|
192
|
+
period = candidate;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return { period: null, timeLeft: null };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const elapsed = Math.floor(nowMs / 1000) % period;
|
|
200
|
+
return {
|
|
201
|
+
period,
|
|
202
|
+
timeLeft: elapsed === 0 ? period : period - elapsed,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
candidateMatchesTerm(item, terms) {
|
|
206
|
+
const id = item.id;
|
|
207
|
+
const name = item.name;
|
|
208
|
+
const login = item.login && typeof item.login === 'object'
|
|
209
|
+
? item.login
|
|
210
|
+
: null;
|
|
211
|
+
const username = login?.username;
|
|
212
|
+
return terms.some((term) => id === term || name === term || username === term);
|
|
213
|
+
}
|
|
214
|
+
async resolveTotpConfigForSession(session, term) {
|
|
215
|
+
const direct = await this.bw
|
|
216
|
+
.runForSession(session, ['get', 'item', term], { timeoutMs: 60_000 })
|
|
217
|
+
.then(({ stdout }) => this.parseBwJson(stdout))
|
|
218
|
+
.catch(() => null);
|
|
219
|
+
const directTotp = this.extractLoginTotp(direct);
|
|
220
|
+
if (directTotp)
|
|
221
|
+
return directTotp;
|
|
222
|
+
const tokens = term
|
|
223
|
+
.split('|')
|
|
224
|
+
.map((token) => token.trim())
|
|
225
|
+
.filter((token) => token.length > 0);
|
|
226
|
+
const terms = tokens.length ? tokens : [term];
|
|
227
|
+
const byId = new Map();
|
|
228
|
+
for (const searchTerm of terms) {
|
|
229
|
+
const { stdout } = await this.bw.runForSession(session, ['list', 'items', '--search', searchTerm], { timeoutMs: 120_000 });
|
|
230
|
+
const results = this.parseBwJson(stdout);
|
|
231
|
+
for (const raw of results) {
|
|
232
|
+
if (!raw || typeof raw !== 'object')
|
|
233
|
+
continue;
|
|
234
|
+
const item = raw;
|
|
235
|
+
if (item.type !== ITEM_TYPE.login)
|
|
236
|
+
continue;
|
|
237
|
+
const id = item.id;
|
|
238
|
+
if (typeof id === 'string' && id.length > 0)
|
|
239
|
+
byId.set(id, item);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const candidates = [...byId.values()];
|
|
243
|
+
const candidate = candidates.find((item) => this.candidateMatchesTerm(item, terms)) ??
|
|
244
|
+
candidates[0];
|
|
245
|
+
if (!candidate || typeof candidate.id !== 'string')
|
|
246
|
+
return null;
|
|
247
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'item', candidate.id], { timeoutMs: 60_000 });
|
|
248
|
+
const item = this.parseBwJson(stdout);
|
|
249
|
+
return this.extractLoginTotp(item);
|
|
173
250
|
}
|
|
174
251
|
parseBwJson(stdout) {
|
|
175
252
|
try {
|
|
@@ -848,11 +925,20 @@ export class KeychainSdk {
|
|
|
848
925
|
});
|
|
849
926
|
}
|
|
850
927
|
async getTotp(input, opts = {}) {
|
|
851
|
-
if (!opts.reveal)
|
|
852
|
-
return
|
|
928
|
+
if (!opts.reveal) {
|
|
929
|
+
return {
|
|
930
|
+
...this.valueResult(null, false),
|
|
931
|
+
period: null,
|
|
932
|
+
timeLeft: null,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
853
935
|
return this.bw.withSession(async (session) => {
|
|
854
936
|
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'totp', input.term], { timeoutMs: 60_000 });
|
|
855
|
-
|
|
937
|
+
const rawTotp = await this.resolveTotpConfigForSession(session, input.term).catch(() => null);
|
|
938
|
+
return {
|
|
939
|
+
...this.valueResult(stdout.trim(), true),
|
|
940
|
+
...this.computeTotpMetadata(rawTotp),
|
|
941
|
+
};
|
|
856
942
|
});
|
|
857
943
|
}
|
|
858
944
|
/** Always reveals — URIs are not considered secrets by Bitwarden. */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { resolveBundledBwBin } from '../bw/resolveBwBin.js';
|
|
3
|
+
export function prepareBwStartup(env = process.env, deps = {}) {
|
|
4
|
+
if (!env.BW_BIN) {
|
|
5
|
+
const resolveBundled = deps.resolveBundledBwBin ?? resolveBundledBwBin;
|
|
6
|
+
const candidate = resolveBundled();
|
|
7
|
+
if (candidate) {
|
|
8
|
+
env.BW_BIN = candidate;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if (env.BW_BIN)
|
|
13
|
+
return;
|
|
14
|
+
const probe = (deps.probeSystemBw ?? spawnSync)('bw', ['--version'], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
});
|
|
17
|
+
if (!probe.error)
|
|
18
|
+
return;
|
|
19
|
+
const warn = deps.warn ?? console.warn;
|
|
20
|
+
warn('[warden-mcp] WARNING: bw CLI not found.\n' +
|
|
21
|
+
'Install it with: npm install -g @bitwarden/cli\n' +
|
|
22
|
+
'Or set the BW_BIN environment variable to the path of the bw binary.\n' +
|
|
23
|
+
'The server will start but tool calls will fail until bw is available.');
|
|
24
|
+
}
|
|
@@ -35,8 +35,8 @@ export function registerTools(server, deps) {
|
|
|
35
35
|
function effectiveReveal(input) {
|
|
36
36
|
return isNoReveal ? false : (input.reveal ?? false);
|
|
37
37
|
}
|
|
38
|
-
function toolResult(kind, value, revealed) {
|
|
39
|
-
return { result: { kind, value, revealed } };
|
|
38
|
+
function toolResult(kind, value, revealed, extra) {
|
|
39
|
+
return { result: { kind, value, revealed, ...(extra ?? {}) } };
|
|
40
40
|
}
|
|
41
41
|
const uriMatchSchema = z.enum([
|
|
42
42
|
'domain',
|
|
@@ -926,7 +926,10 @@ export function registerTools(server, deps) {
|
|
|
926
926
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
927
927
|
const totp = await sdk.getTotp({ term: input.term }, { reveal: effectiveReveal(input) });
|
|
928
928
|
return {
|
|
929
|
-
structuredContent: toolResult('totp', totp.value, totp.revealed
|
|
929
|
+
structuredContent: toolResult('totp', totp.value, totp.revealed, {
|
|
930
|
+
period: totp.period,
|
|
931
|
+
timeLeft: totp.timeLeft,
|
|
932
|
+
}),
|
|
930
933
|
content: [{ type: 'text', text: 'OK' }],
|
|
931
934
|
};
|
|
932
935
|
});
|
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.18",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Vaultwarden/Bitwarden MCP server backed by Bitwarden CLI (bw).",
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist/",
|
|
31
31
|
"bin/",
|
|
32
|
+
"patches/",
|
|
32
33
|
"!dist/**/*.test.js",
|
|
33
34
|
"!dist/integration/"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"dev": "tsx watch --clear-screen=false src/server.ts",
|
|
38
|
+
"postinstall": "node bin/patch-bitwarden-cli.js",
|
|
37
39
|
"build": "tsc -p .",
|
|
38
40
|
"start": "node dist/server.js",
|
|
39
41
|
"test": "npm run build && node --test \"dist/**/*.test.js\"",
|
|
@@ -45,10 +47,11 @@
|
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
47
49
|
"express": "^5.2.1",
|
|
50
|
+
"patch-package": "^8.0.1",
|
|
48
51
|
"zod": "^4.3.6"
|
|
49
52
|
},
|
|
50
53
|
"optionalDependencies": {
|
|
51
|
-
"@bitwarden/cli": "2026.
|
|
54
|
+
"@bitwarden/cli": "2026.2.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@biomejs/biome": "^2.4.8",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
diff --git a/node_modules/@bitwarden/cli/build/bw.js b/node_modules/@bitwarden/cli/build/bw.js
|
|
2
|
+
index 6b02f68..2085f5e 100755
|
|
3
|
+
--- a/node_modules/@bitwarden/cli/build/bw.js
|
|
4
|
+
+++ b/node_modules/@bitwarden/cli/build/bw.js
|
|
5
|
+
@@ -27093,7 +27093,17 @@ class AuthRequestLoginStrategy extends LoginStrategy {
|
|
6
|
+
}
|
|
7
|
+
setAccountCryptographicState(response, userId) {
|
|
8
|
+
return auth_request_login_strategy_awaiter(this, void 0, void 0, function* () {
|
|
9
|
+
- yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
10
|
+
+ /* icoretech-vaultwarden-compat */
|
|
11
|
+
+ if (response.accountKeysResponseModel) {
|
|
12
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
13
|
+
+ }
|
|
14
|
+
+ else if (response.privateKey) {
|
|
15
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState({
|
|
16
|
+
+ V1: {
|
|
17
|
+
+ private_key: response.privateKey,
|
|
18
|
+
+ },
|
|
19
|
+
+ }, userId);
|
|
20
|
+
+ }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
exportCache() {
|
|
24
|
+
@@ -27203,7 +27213,17 @@ class PasswordLoginStrategy extends LoginStrategy {
|
|
25
|
+
}
|
|
26
|
+
setAccountCryptographicState(response, userId) {
|
|
27
|
+
return password_login_strategy_awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
- yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
29
|
+
+ /* icoretech-vaultwarden-compat */
|
|
30
|
+
+ if (response.accountKeysResponseModel) {
|
|
31
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
32
|
+
+ }
|
|
33
|
+
+ else if (response.privateKey) {
|
|
34
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState({
|
|
35
|
+
+ V1: {
|
|
36
|
+
+ private_key: response.privateKey,
|
|
37
|
+
+ },
|
|
38
|
+
+ }, userId);
|
|
39
|
+
+ }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
encryptionKeyMigrationRequired(response) {
|
|
43
|
+
@@ -28327,7 +28347,17 @@ class UserApiLoginStrategy extends LoginStrategy {
|
|
44
|
+
}
|
|
45
|
+
setAccountCryptographicState(response, userId) {
|
|
46
|
+
return user_api_login_strategy_awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
- yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
48
|
+
+ /* icoretech-vaultwarden-compat */
|
|
49
|
+
+ if (response.accountKeysResponseModel) {
|
|
50
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
51
|
+
+ }
|
|
52
|
+
+ else if (response.privateKey) {
|
|
53
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState({
|
|
54
|
+
+ V1: {
|
|
55
|
+
+ private_key: response.privateKey,
|
|
56
|
+
+ },
|
|
57
|
+
+ }, userId);
|
|
58
|
+
+ }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Overridden to save client ID and secret to token service
|
|
62
|
+
@@ -28457,7 +28487,17 @@ class WebAuthnLoginStrategy extends LoginStrategy {
|
|
63
|
+
}
|
|
64
|
+
setAccountCryptographicState(response, userId) {
|
|
65
|
+
return webauthn_login_strategy_awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
- yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
67
|
+
+ /* icoretech-vaultwarden-compat */
|
|
68
|
+
+ if (response.accountKeysResponseModel) {
|
|
69
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState(response.accountKeysResponseModel.toWrappedAccountCryptographicState(), userId);
|
|
70
|
+
+ }
|
|
71
|
+
+ else if (response.privateKey) {
|
|
72
|
+
+ yield this.accountCryptographicStateService.setAccountCryptographicState({
|
|
73
|
+
+ V1: {
|
|
74
|
+
+ private_key: response.privateKey,
|
|
75
|
+
+ },
|
|
76
|
+
+ }, userId);
|
|
77
|
+
+ }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
exportCache() {
|