@bitwarden/mcp-server 2025.7.0-beta.1
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/LICENSE.txt +674 -0
- package/README.md +153 -0
- package/dist/index.js +973 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// Zod schemas for validating Bitwarden CLI tool inputs
|
|
9
|
+
// Schema for validating 'lock' command parameters (no parameters required)
|
|
10
|
+
const lockSchema = z.object({});
|
|
11
|
+
// Schema for validating 'unlock' command parameters
|
|
12
|
+
const unlockSchema = z.object({
|
|
13
|
+
// Master password for unlocking the vault
|
|
14
|
+
password: z.string().min(1, 'Password is required'),
|
|
15
|
+
});
|
|
16
|
+
// Schema for validating 'sync' command parameters (no parameters required)
|
|
17
|
+
const syncSchema = z.object({});
|
|
18
|
+
// Schema for validating 'status' command parameters (no parameters required)
|
|
19
|
+
const statusSchema = z.object({});
|
|
20
|
+
// Schema for validating 'list' command parameters
|
|
21
|
+
const listSchema = z.object({
|
|
22
|
+
// Type of items to list from the vault
|
|
23
|
+
type: z.enum(['items', 'folders', 'collections', 'organizations']),
|
|
24
|
+
// Optional search term to filter results
|
|
25
|
+
search: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
// Schema for validating 'get' command parameters
|
|
28
|
+
const getSchema = z.object({
|
|
29
|
+
// Type of object to retrieve from the vault
|
|
30
|
+
object: z.enum([
|
|
31
|
+
'item',
|
|
32
|
+
'username',
|
|
33
|
+
'password',
|
|
34
|
+
'uri',
|
|
35
|
+
'totp',
|
|
36
|
+
'notes',
|
|
37
|
+
'exposed',
|
|
38
|
+
'attachment',
|
|
39
|
+
'folder',
|
|
40
|
+
'collection',
|
|
41
|
+
'organization',
|
|
42
|
+
]),
|
|
43
|
+
// ID or search term to identify the object
|
|
44
|
+
id: z.string().min(1, 'ID or search term is required'),
|
|
45
|
+
});
|
|
46
|
+
// Schema for validating 'generate' command parameters
|
|
47
|
+
const generateSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
// Length of the generated password (minimum 5)
|
|
50
|
+
length: z.number().int().min(5).optional(),
|
|
51
|
+
// Include uppercase characters in the password
|
|
52
|
+
uppercase: z.boolean().optional(),
|
|
53
|
+
// Include lowercase characters in the password
|
|
54
|
+
lowercase: z.boolean().optional(),
|
|
55
|
+
// Include numbers in the password
|
|
56
|
+
number: z.boolean().optional(),
|
|
57
|
+
// Include special characters in the password
|
|
58
|
+
special: z.boolean().optional(),
|
|
59
|
+
// Generate a passphrase instead of a password
|
|
60
|
+
passphrase: z.boolean().optional(),
|
|
61
|
+
// Number of words to include in the passphrase
|
|
62
|
+
words: z.number().int().min(1).optional(),
|
|
63
|
+
// Character to use between words in the passphrase
|
|
64
|
+
separator: z.string().optional(),
|
|
65
|
+
// Capitalize the first letter of each word in the passphrase
|
|
66
|
+
capitalize: z.boolean().optional(),
|
|
67
|
+
})
|
|
68
|
+
.refine((data) => {
|
|
69
|
+
// If passphrase is true, words and separator are relevant
|
|
70
|
+
// If not, then length, uppercase, lowercase, etc. are relevant
|
|
71
|
+
if (data.passphrase) {
|
|
72
|
+
return true; // Accept any combination for passphrase
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
return true; // Accept any combination for regular password
|
|
76
|
+
}
|
|
77
|
+
}, {
|
|
78
|
+
message: 'Provide valid options based on whether generating a passphrase or password',
|
|
79
|
+
});
|
|
80
|
+
// Schema for validating URI objects in login items
|
|
81
|
+
const uriSchema = z.object({
|
|
82
|
+
// URI associated with the login (e.g., https://example.com)
|
|
83
|
+
uri: z.string().url('Must be a valid URL'),
|
|
84
|
+
// URI match type for auto-fill functionality (0: Domain, 1: Host, 2: Starts With, 3: Exact, 4: Regular Expression, 5: Never)
|
|
85
|
+
match: z
|
|
86
|
+
.union([
|
|
87
|
+
z.literal(0),
|
|
88
|
+
z.literal(1),
|
|
89
|
+
z.literal(2),
|
|
90
|
+
z.literal(3),
|
|
91
|
+
z.literal(4),
|
|
92
|
+
z.literal(5),
|
|
93
|
+
])
|
|
94
|
+
.optional(),
|
|
95
|
+
});
|
|
96
|
+
// Schema for validating login information in vault items
|
|
97
|
+
const loginSchema = z.object({
|
|
98
|
+
// Username for the login
|
|
99
|
+
username: z.string().optional(),
|
|
100
|
+
// Password for the login
|
|
101
|
+
password: z.string().optional(),
|
|
102
|
+
// List of URIs associated with the login
|
|
103
|
+
uris: z.array(uriSchema).optional(),
|
|
104
|
+
// Time-based one-time password (TOTP) secret
|
|
105
|
+
totp: z.string().optional(),
|
|
106
|
+
});
|
|
107
|
+
// Schema for validating 'create' command parameters
|
|
108
|
+
const createSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
// Name of the item/folder to create
|
|
111
|
+
name: z.string().min(1, 'Name is required'),
|
|
112
|
+
// Type of object to create: 'item' or 'folder'
|
|
113
|
+
objectType: z.enum(['item', 'folder']),
|
|
114
|
+
// Type of item to create (only for items)
|
|
115
|
+
type: z
|
|
116
|
+
.union([
|
|
117
|
+
z.literal(1), // Login
|
|
118
|
+
z.literal(2), // Secure Note
|
|
119
|
+
z.literal(3), // Card
|
|
120
|
+
z.literal(4), // Identity
|
|
121
|
+
])
|
|
122
|
+
.optional(),
|
|
123
|
+
// Optional notes for the item
|
|
124
|
+
notes: z.string().optional(),
|
|
125
|
+
// Login details (required when type is 1)
|
|
126
|
+
login: loginSchema.optional(),
|
|
127
|
+
})
|
|
128
|
+
.refine((data) => {
|
|
129
|
+
// If objectType is item, type should be provided
|
|
130
|
+
if (data.objectType === 'item') {
|
|
131
|
+
if (!data.type) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// If type is login (1), login object should be provided
|
|
135
|
+
if (data.type === 1) {
|
|
136
|
+
return !!data.login; // login object should exist
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Notes should only be provided for items, not folders
|
|
140
|
+
if (data.objectType === 'folder' && data.notes) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
// Login should only be provided for items, not folders
|
|
144
|
+
if (data.objectType === 'folder' && data.login) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}, {
|
|
149
|
+
message: 'Item type is required for items, login details are required for login items, and notes/login are only valid for items',
|
|
150
|
+
});
|
|
151
|
+
// Schema for validating login fields during item editing
|
|
152
|
+
const editLoginSchema = z.object({
|
|
153
|
+
// New username for the login
|
|
154
|
+
username: z.string().optional(),
|
|
155
|
+
// New password for the login
|
|
156
|
+
password: z.string().optional(),
|
|
157
|
+
});
|
|
158
|
+
// Schema for validating 'edit' command parameters
|
|
159
|
+
const editSchema = z
|
|
160
|
+
.object({
|
|
161
|
+
// Type of object to edit: 'item' or 'folder'
|
|
162
|
+
objectType: z.enum(['item', 'folder']),
|
|
163
|
+
// ID of the item/folder to edit
|
|
164
|
+
id: z.string().min(1, 'ID is required'),
|
|
165
|
+
// New name for the item/folder
|
|
166
|
+
name: z.string().optional(),
|
|
167
|
+
// New notes for the item
|
|
168
|
+
notes: z.string().optional(),
|
|
169
|
+
// Updated login information (only for items)
|
|
170
|
+
login: editLoginSchema.optional(),
|
|
171
|
+
})
|
|
172
|
+
.refine((data) => {
|
|
173
|
+
// Notes should only be provided for items, not folders
|
|
174
|
+
if (data.objectType === 'folder' && data.notes) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Login should only be provided for items, not folders
|
|
178
|
+
if (data.objectType === 'folder' && data.login) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}, {
|
|
183
|
+
message: 'Notes and login information are only valid for items, not folders',
|
|
184
|
+
});
|
|
185
|
+
// Schema for validating 'delete' command parameters
|
|
186
|
+
const deleteSchema = z.object({
|
|
187
|
+
// Type of object to delete
|
|
188
|
+
object: z.enum(['item', 'attachment', 'folder', 'org-collection']),
|
|
189
|
+
// ID of the object to delete
|
|
190
|
+
id: z.string().min(1, 'Object ID is required'),
|
|
191
|
+
// Whether to permanently delete the item (skip trash)
|
|
192
|
+
permanent: z.boolean().optional(),
|
|
193
|
+
});
|
|
194
|
+
// Define tools
|
|
195
|
+
const lockTool = {
|
|
196
|
+
name: 'lock',
|
|
197
|
+
description: 'Lock the vault',
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
const unlockTool = {
|
|
204
|
+
name: 'unlock',
|
|
205
|
+
description: 'Unlock the vault with your master password',
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
password: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Master password for the vault',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ['password'],
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const syncTool = {
|
|
218
|
+
name: 'sync',
|
|
219
|
+
description: 'Sync vault data from the Bitwarden server',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const statusTool = {
|
|
226
|
+
name: 'status',
|
|
227
|
+
description: 'Check the status of the Bitwarden CLI',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const listTool = {
|
|
234
|
+
name: 'list',
|
|
235
|
+
description: 'List items from your vault',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
type: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
description: 'Type of items to list (items, folders, collections, organizations)',
|
|
242
|
+
enum: ['items', 'folders', 'collections', 'organizations'],
|
|
243
|
+
},
|
|
244
|
+
search: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Optional search term to filter results',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
required: ['type'],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const getTool = {
|
|
253
|
+
name: 'get',
|
|
254
|
+
description: 'Get a specific item from your vault',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
object: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: 'Type of object to retrieve',
|
|
261
|
+
enum: [
|
|
262
|
+
'item',
|
|
263
|
+
'username',
|
|
264
|
+
'password',
|
|
265
|
+
'uri',
|
|
266
|
+
'totp',
|
|
267
|
+
'notes',
|
|
268
|
+
'exposed',
|
|
269
|
+
'attachment',
|
|
270
|
+
'folder',
|
|
271
|
+
'collection',
|
|
272
|
+
'organization',
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
id: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
description: 'ID or search term for the object',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ['object', 'id'],
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
const generateTool = {
|
|
284
|
+
name: 'generate',
|
|
285
|
+
description: 'Generate a secure password or passphrase',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
length: {
|
|
290
|
+
type: 'number',
|
|
291
|
+
description: 'Length of the password (minimum 5)',
|
|
292
|
+
minimum: 5,
|
|
293
|
+
},
|
|
294
|
+
uppercase: {
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
description: 'Include uppercase characters',
|
|
297
|
+
},
|
|
298
|
+
lowercase: {
|
|
299
|
+
type: 'boolean',
|
|
300
|
+
description: 'Include lowercase characters',
|
|
301
|
+
},
|
|
302
|
+
number: {
|
|
303
|
+
type: 'boolean',
|
|
304
|
+
description: 'Include numeric characters',
|
|
305
|
+
},
|
|
306
|
+
special: {
|
|
307
|
+
type: 'boolean',
|
|
308
|
+
description: 'Include special characters',
|
|
309
|
+
},
|
|
310
|
+
passphrase: {
|
|
311
|
+
type: 'boolean',
|
|
312
|
+
description: 'Generate a passphrase instead of a password',
|
|
313
|
+
},
|
|
314
|
+
words: {
|
|
315
|
+
type: 'number',
|
|
316
|
+
description: 'Number of words in the passphrase',
|
|
317
|
+
},
|
|
318
|
+
separator: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'Character that separates words in the passphrase',
|
|
321
|
+
},
|
|
322
|
+
capitalize: {
|
|
323
|
+
type: 'boolean',
|
|
324
|
+
description: 'Capitalize the first letter of each word in the passphrase',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
const createTool = {
|
|
330
|
+
name: 'create',
|
|
331
|
+
description: 'Create a new item or folder in your vault',
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
objectType: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'Type of object to create',
|
|
338
|
+
enum: ['item', 'folder'],
|
|
339
|
+
},
|
|
340
|
+
name: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
description: 'Name of the item or folder',
|
|
343
|
+
},
|
|
344
|
+
type: {
|
|
345
|
+
type: 'number',
|
|
346
|
+
description: 'Type of item (1: Login, 2: Secure Note, 3: Card, 4: Identity) - required for items',
|
|
347
|
+
enum: [1, 2, 3, 4],
|
|
348
|
+
},
|
|
349
|
+
notes: {
|
|
350
|
+
type: 'string',
|
|
351
|
+
description: 'Notes for the item (only valid for items, not folders)',
|
|
352
|
+
},
|
|
353
|
+
login: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
description: 'Login information (required for type=1)',
|
|
356
|
+
properties: {
|
|
357
|
+
username: {
|
|
358
|
+
type: 'string',
|
|
359
|
+
description: 'Username for the login',
|
|
360
|
+
},
|
|
361
|
+
password: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
description: 'Password for the login',
|
|
364
|
+
},
|
|
365
|
+
uris: {
|
|
366
|
+
type: 'array',
|
|
367
|
+
description: 'List of URIs associated with the login',
|
|
368
|
+
items: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
uri: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
description: 'URI for the login (e.g., https://example.com)',
|
|
374
|
+
},
|
|
375
|
+
match: {
|
|
376
|
+
type: 'number',
|
|
377
|
+
description: 'URI match type (0: Domain, 1: Host, 2: Starts With, 3: Exact, 4: Regular Expression, 5: Never)',
|
|
378
|
+
enum: [0, 1, 2, 3, 4, 5],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required: ['uri'],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
totp: {
|
|
385
|
+
type: 'string',
|
|
386
|
+
description: 'TOTP secret for the login',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
required: ['objectType', 'name'],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
const editTool = {
|
|
395
|
+
name: 'edit',
|
|
396
|
+
description: 'Edit an existing item or folder in your vault',
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: {
|
|
400
|
+
objectType: {
|
|
401
|
+
type: 'string',
|
|
402
|
+
description: 'Type of object to edit',
|
|
403
|
+
enum: ['item', 'folder'],
|
|
404
|
+
},
|
|
405
|
+
id: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: 'ID of the item or folder to edit',
|
|
408
|
+
},
|
|
409
|
+
name: {
|
|
410
|
+
type: 'string',
|
|
411
|
+
description: 'New name for the item or folder',
|
|
412
|
+
},
|
|
413
|
+
notes: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
description: 'New notes for the item (only valid for items, not folders)',
|
|
416
|
+
},
|
|
417
|
+
login: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
description: 'Login information to update (only for items)',
|
|
420
|
+
properties: {
|
|
421
|
+
username: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
description: 'New username for the login',
|
|
424
|
+
},
|
|
425
|
+
password: {
|
|
426
|
+
type: 'string',
|
|
427
|
+
description: 'New password for the login',
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
required: ['objectType', 'id'],
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
const deleteTool = {
|
|
436
|
+
name: 'delete',
|
|
437
|
+
description: 'Delete an item from your vault',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: {
|
|
441
|
+
object: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
description: 'Type of object to delete',
|
|
444
|
+
enum: ['item', 'attachment', 'folder', 'org-collection'],
|
|
445
|
+
},
|
|
446
|
+
id: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
description: 'ID of the object to delete',
|
|
449
|
+
},
|
|
450
|
+
permanent: {
|
|
451
|
+
type: 'boolean',
|
|
452
|
+
description: 'Permanently delete the item instead of moving to trash',
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
required: ['object', 'id'],
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Validates input against a Zod schema and returns either the validated data or a structured error response.
|
|
460
|
+
*
|
|
461
|
+
* @template T - Type of the validated output
|
|
462
|
+
* @param {z.ZodType<T>} schema - The Zod schema to validate against
|
|
463
|
+
* @param {unknown} args - The input arguments to validate
|
|
464
|
+
* @returns {[true, T] | [false, { content: Array<{ type: string; text: string }>; isError: true }]}
|
|
465
|
+
* A tuple with either:
|
|
466
|
+
* - [true, validatedData] if validation succeeds
|
|
467
|
+
* - [false, errorObject] if validation fails
|
|
468
|
+
*/
|
|
469
|
+
export function validateInput(schema, args) {
|
|
470
|
+
try {
|
|
471
|
+
const validatedInput = schema.parse(args || {});
|
|
472
|
+
return [true, validatedInput];
|
|
473
|
+
}
|
|
474
|
+
catch (validationError) {
|
|
475
|
+
if (validationError instanceof z.ZodError) {
|
|
476
|
+
return [
|
|
477
|
+
false,
|
|
478
|
+
{
|
|
479
|
+
content: [
|
|
480
|
+
{
|
|
481
|
+
type: 'text',
|
|
482
|
+
text: `Validation error: ${validationError.errors.map((e) => e.message).join(', ')}`,
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
isError: true,
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
// Re-throw any non-ZodError
|
|
490
|
+
throw validationError;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const execPromise = promisify(exec);
|
|
494
|
+
/**
|
|
495
|
+
* Executes a Bitwarden CLI command and returns the result.
|
|
496
|
+
*
|
|
497
|
+
* @async
|
|
498
|
+
* @param {string} command - The Bitwarden CLI command to execute (without 'bw' prefix)
|
|
499
|
+
* @returns {Promise<CliResponse>} A promise that resolves to an object containing output and/or error output
|
|
500
|
+
*/
|
|
501
|
+
async function executeCliCommand(command) {
|
|
502
|
+
try {
|
|
503
|
+
const { stdout, stderr } = await execPromise(`bw ${command}`);
|
|
504
|
+
return {
|
|
505
|
+
output: stdout,
|
|
506
|
+
errorOutput: stderr,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
if (e instanceof Error) {
|
|
511
|
+
return {
|
|
512
|
+
errorOutput: e.message,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
errorOutput: 'An error occurred while executing the command',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Initializes and starts the MCP server for handling Bitwarden CLI commands.
|
|
522
|
+
* Requires the BW_SESSION environment variable to be set.
|
|
523
|
+
*
|
|
524
|
+
* @async
|
|
525
|
+
* @returns {Promise<void>}
|
|
526
|
+
*/
|
|
527
|
+
async function runServer() {
|
|
528
|
+
// Require session from environment variable
|
|
529
|
+
if (!process.env.BW_SESSION) {
|
|
530
|
+
console.error('Please set the BW_SESSION environment variable');
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
// Set up server
|
|
534
|
+
console.error('Bitwarden MCP Server starting ...');
|
|
535
|
+
const server = new Server({
|
|
536
|
+
name: 'Bitwarden MCP Server',
|
|
537
|
+
version: '2025.7.0',
|
|
538
|
+
}, {
|
|
539
|
+
capabilities: {
|
|
540
|
+
tools: {},
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
544
|
+
try {
|
|
545
|
+
const { name } = request.params;
|
|
546
|
+
switch (name) {
|
|
547
|
+
case 'lock': {
|
|
548
|
+
// Validate inputs
|
|
549
|
+
const [isValid, validationResult] = validateInput(lockSchema, request.params.arguments);
|
|
550
|
+
if (!isValid) {
|
|
551
|
+
return validationResult;
|
|
552
|
+
}
|
|
553
|
+
const result = await executeCliCommand('lock');
|
|
554
|
+
return {
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: 'text',
|
|
558
|
+
text: result.output || result.errorOutput,
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
isError: result.errorOutput ? true : false,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
case 'unlock': {
|
|
565
|
+
// Validate inputs
|
|
566
|
+
const [isValid, validationResult] = validateInput(unlockSchema, request.params.arguments);
|
|
567
|
+
if (!isValid) {
|
|
568
|
+
return validationResult;
|
|
569
|
+
}
|
|
570
|
+
const { password } = validationResult;
|
|
571
|
+
// Use echo to pipe password to bw unlock
|
|
572
|
+
const result = await executeCliCommand(`unlock "${password}" --raw`);
|
|
573
|
+
return {
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: 'text',
|
|
577
|
+
text: result.output ||
|
|
578
|
+
result.errorOutput ||
|
|
579
|
+
'Vault unlocked successfully',
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
isError: result.errorOutput ? true : false,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
case 'sync': {
|
|
586
|
+
// Validate inputs
|
|
587
|
+
const [isValid, validationResult] = validateInput(syncSchema, request.params.arguments);
|
|
588
|
+
if (!isValid) {
|
|
589
|
+
return validationResult;
|
|
590
|
+
}
|
|
591
|
+
const result = await executeCliCommand('sync');
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: 'text',
|
|
596
|
+
text: result.output ||
|
|
597
|
+
result.errorOutput ||
|
|
598
|
+
'Vault synced successfully',
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
isError: result.errorOutput ? true : false,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
case 'status': {
|
|
605
|
+
// Validate inputs
|
|
606
|
+
const [isValid, validationResult] = validateInput(statusSchema, request.params.arguments);
|
|
607
|
+
if (!isValid) {
|
|
608
|
+
return validationResult;
|
|
609
|
+
}
|
|
610
|
+
const result = await executeCliCommand('status');
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: 'text',
|
|
615
|
+
text: result.output || result.errorOutput,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
isError: result.errorOutput ? true : false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
case 'list': {
|
|
622
|
+
// Validate inputs
|
|
623
|
+
const [isValid, validationResult] = validateInput(listSchema, request.params.arguments);
|
|
624
|
+
if (!isValid) {
|
|
625
|
+
return validationResult;
|
|
626
|
+
}
|
|
627
|
+
const { type, search } = validationResult;
|
|
628
|
+
// Construct the command with the optional search parameter
|
|
629
|
+
let command = `list ${type}`;
|
|
630
|
+
if (search) {
|
|
631
|
+
command += ` --search "${search}"`;
|
|
632
|
+
}
|
|
633
|
+
const result = await executeCliCommand(command);
|
|
634
|
+
return {
|
|
635
|
+
content: [
|
|
636
|
+
{
|
|
637
|
+
type: 'text',
|
|
638
|
+
text: result.output || result.errorOutput,
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
isError: result.errorOutput ? true : false,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
case 'get': {
|
|
645
|
+
// Validate inputs
|
|
646
|
+
const [isValid, validationResult] = validateInput(getSchema, request.params.arguments);
|
|
647
|
+
if (!isValid) {
|
|
648
|
+
return validationResult;
|
|
649
|
+
}
|
|
650
|
+
const { object, id } = validationResult;
|
|
651
|
+
const result = await executeCliCommand(`get ${object} "${id}"`);
|
|
652
|
+
return {
|
|
653
|
+
content: [
|
|
654
|
+
{
|
|
655
|
+
type: 'text',
|
|
656
|
+
text: result.output || result.errorOutput,
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
isError: result.errorOutput ? true : false,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
case 'generate': {
|
|
663
|
+
// Validate inputs
|
|
664
|
+
const [isValid, validationResult] = validateInput(generateSchema, request.params.arguments);
|
|
665
|
+
if (!isValid) {
|
|
666
|
+
return validationResult;
|
|
667
|
+
}
|
|
668
|
+
const args = validationResult;
|
|
669
|
+
let command = 'generate';
|
|
670
|
+
if (args.passphrase) {
|
|
671
|
+
command += ' --passphrase';
|
|
672
|
+
if (args.words) {
|
|
673
|
+
command += ` --words ${args.words}`;
|
|
674
|
+
}
|
|
675
|
+
if (args.separator) {
|
|
676
|
+
command += ` --separator "${args.separator}"`;
|
|
677
|
+
}
|
|
678
|
+
if (args.capitalize) {
|
|
679
|
+
command += ' --capitalize';
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// Regular password generation
|
|
684
|
+
if (args.uppercase) {
|
|
685
|
+
command += ' --uppercase';
|
|
686
|
+
}
|
|
687
|
+
if (args.lowercase) {
|
|
688
|
+
command += ' --lowercase';
|
|
689
|
+
}
|
|
690
|
+
if (args.number) {
|
|
691
|
+
command += ' --number';
|
|
692
|
+
}
|
|
693
|
+
if (args.special) {
|
|
694
|
+
command += ' --special';
|
|
695
|
+
}
|
|
696
|
+
if (args.length) {
|
|
697
|
+
command += ` --length ${args.length}`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const result = await executeCliCommand(command);
|
|
701
|
+
return {
|
|
702
|
+
content: [
|
|
703
|
+
{
|
|
704
|
+
type: 'text',
|
|
705
|
+
text: result.output || result.errorOutput,
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
isError: result.errorOutput ? true : false,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
case 'create': {
|
|
712
|
+
// Validate inputs
|
|
713
|
+
const [isValid, validationResult] = validateInput(createSchema, request.params.arguments);
|
|
714
|
+
if (!isValid) {
|
|
715
|
+
return validationResult;
|
|
716
|
+
}
|
|
717
|
+
const { objectType, name: itemName, type: itemType, notes, login, } = validationResult;
|
|
718
|
+
if (objectType === 'folder') {
|
|
719
|
+
// Create folder
|
|
720
|
+
const folderObject = {
|
|
721
|
+
name: itemName,
|
|
722
|
+
};
|
|
723
|
+
const folderJson = JSON.stringify(folderObject);
|
|
724
|
+
const folderBase64 = Buffer.from(folderJson, 'utf8').toString('base64');
|
|
725
|
+
const createCommand = `create folder ${folderBase64}`;
|
|
726
|
+
const result = await executeCliCommand(createCommand);
|
|
727
|
+
return {
|
|
728
|
+
content: [
|
|
729
|
+
{
|
|
730
|
+
type: 'text',
|
|
731
|
+
text: result.output || result.errorOutput,
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
isError: result.errorOutput ? true : false,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// Create item
|
|
739
|
+
const itemObject = {
|
|
740
|
+
name: itemName,
|
|
741
|
+
type: itemType,
|
|
742
|
+
};
|
|
743
|
+
if (notes) {
|
|
744
|
+
itemObject.notes = notes;
|
|
745
|
+
}
|
|
746
|
+
// Add login properties for login items
|
|
747
|
+
if (itemType === 1 && login) {
|
|
748
|
+
itemObject.login = {};
|
|
749
|
+
if (login.username) {
|
|
750
|
+
itemObject.login.username = login.username;
|
|
751
|
+
}
|
|
752
|
+
if (login.password) {
|
|
753
|
+
itemObject.login.password = login.password;
|
|
754
|
+
}
|
|
755
|
+
if (login.totp) {
|
|
756
|
+
itemObject.login.totp = login.totp;
|
|
757
|
+
}
|
|
758
|
+
if (login.uris && login.uris.length > 0) {
|
|
759
|
+
itemObject.login.uris = login.uris;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const itemJson = JSON.stringify(itemObject);
|
|
763
|
+
const itemBase64 = Buffer.from(itemJson, 'utf8').toString('base64');
|
|
764
|
+
const createCommand = `create item ${itemBase64}`;
|
|
765
|
+
const result = await executeCliCommand(createCommand);
|
|
766
|
+
return {
|
|
767
|
+
content: [
|
|
768
|
+
{
|
|
769
|
+
type: 'text',
|
|
770
|
+
text: result.output || result.errorOutput,
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
isError: result.errorOutput ? true : false,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
case 'edit': {
|
|
778
|
+
// Validate inputs
|
|
779
|
+
const [isValid, validationResult] = validateInput(editSchema, request.params.arguments);
|
|
780
|
+
if (!isValid) {
|
|
781
|
+
return validationResult;
|
|
782
|
+
}
|
|
783
|
+
const { objectType, id, name: itemName, notes, login, } = validationResult;
|
|
784
|
+
if (objectType === 'folder') {
|
|
785
|
+
// Edit folder
|
|
786
|
+
const getResult = await executeCliCommand(`get folder ${id}`);
|
|
787
|
+
if (getResult.errorOutput) {
|
|
788
|
+
return {
|
|
789
|
+
content: [
|
|
790
|
+
{
|
|
791
|
+
type: 'text',
|
|
792
|
+
text: `Error retrieving folder to edit: ${getResult.errorOutput}`,
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
isError: true,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// Parse the current folder
|
|
799
|
+
let currentFolder;
|
|
800
|
+
try {
|
|
801
|
+
currentFolder = JSON.parse(getResult.output || '{}');
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
return {
|
|
805
|
+
content: [
|
|
806
|
+
{
|
|
807
|
+
type: 'text',
|
|
808
|
+
text: `Error parsing folder data: ${error}`,
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
isError: true,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
// Update folder name
|
|
815
|
+
if (itemName) {
|
|
816
|
+
currentFolder.name = itemName;
|
|
817
|
+
}
|
|
818
|
+
const folderJson = JSON.stringify(currentFolder);
|
|
819
|
+
const folderBase64 = Buffer.from(folderJson, 'utf8').toString('base64');
|
|
820
|
+
const editCommand = `edit folder ${id} ${folderBase64}`;
|
|
821
|
+
const result = await executeCliCommand(editCommand);
|
|
822
|
+
return {
|
|
823
|
+
content: [
|
|
824
|
+
{
|
|
825
|
+
type: 'text',
|
|
826
|
+
text: result.output ||
|
|
827
|
+
result.errorOutput ||
|
|
828
|
+
`Folder ${id} updated successfully`,
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
isError: result.errorOutput ? true : false,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
// Edit item
|
|
836
|
+
const getResult = await executeCliCommand(`get item ${id}`);
|
|
837
|
+
if (getResult.errorOutput) {
|
|
838
|
+
return {
|
|
839
|
+
content: [
|
|
840
|
+
{
|
|
841
|
+
type: 'text',
|
|
842
|
+
text: `Error retrieving item to edit: ${getResult.errorOutput}`,
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
isError: true,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
// Parse the current item
|
|
849
|
+
let currentItem;
|
|
850
|
+
try {
|
|
851
|
+
currentItem = JSON.parse(getResult.output || '{}');
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
return {
|
|
855
|
+
content: [
|
|
856
|
+
{
|
|
857
|
+
type: 'text',
|
|
858
|
+
text: `Error parsing item data: ${error}`,
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
isError: true,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// Update fields
|
|
865
|
+
if (itemName) {
|
|
866
|
+
currentItem.name = itemName;
|
|
867
|
+
}
|
|
868
|
+
if (notes) {
|
|
869
|
+
currentItem.notes = notes;
|
|
870
|
+
}
|
|
871
|
+
// Update login fields if this is a login item
|
|
872
|
+
if (currentItem.type === 1 && login) {
|
|
873
|
+
if (!currentItem.login) {
|
|
874
|
+
currentItem.login = {};
|
|
875
|
+
}
|
|
876
|
+
if (login.username) {
|
|
877
|
+
currentItem.login.username = login.username;
|
|
878
|
+
}
|
|
879
|
+
if (login.password) {
|
|
880
|
+
currentItem.login.password = login.password;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// Perform the edit
|
|
884
|
+
const itemJson = JSON.stringify(currentItem);
|
|
885
|
+
const itemBase64 = Buffer.from(itemJson, 'utf8').toString('base64');
|
|
886
|
+
const editCommand = `edit item ${id} ${itemBase64}`;
|
|
887
|
+
const result = await executeCliCommand(editCommand);
|
|
888
|
+
return {
|
|
889
|
+
content: [
|
|
890
|
+
{
|
|
891
|
+
type: 'text',
|
|
892
|
+
text: result.output ||
|
|
893
|
+
result.errorOutput ||
|
|
894
|
+
`Item ${id} updated successfully`,
|
|
895
|
+
},
|
|
896
|
+
],
|
|
897
|
+
isError: result.errorOutput ? true : false,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
case 'delete': {
|
|
902
|
+
// Validate inputs
|
|
903
|
+
const [isValid, validationResult] = validateInput(deleteSchema, request.params.arguments);
|
|
904
|
+
if (!isValid) {
|
|
905
|
+
return validationResult;
|
|
906
|
+
}
|
|
907
|
+
const { object, id, permanent } = validationResult;
|
|
908
|
+
let command = `delete ${object} ${id}`;
|
|
909
|
+
if (permanent) {
|
|
910
|
+
command += ' --permanent';
|
|
911
|
+
}
|
|
912
|
+
const result = await executeCliCommand(command);
|
|
913
|
+
return {
|
|
914
|
+
content: [
|
|
915
|
+
{
|
|
916
|
+
type: 'text',
|
|
917
|
+
text: result.output ||
|
|
918
|
+
result.errorOutput ||
|
|
919
|
+
`${object} ${id} deleted successfully`,
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
isError: result.errorOutput ? true : false,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
default:
|
|
926
|
+
return {
|
|
927
|
+
content: [
|
|
928
|
+
{
|
|
929
|
+
type: 'text',
|
|
930
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
isError: true,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
console.error('Error handling tool request:', error);
|
|
939
|
+
return {
|
|
940
|
+
content: [
|
|
941
|
+
{
|
|
942
|
+
type: 'text',
|
|
943
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
isError: true,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
951
|
+
return {
|
|
952
|
+
tools: [
|
|
953
|
+
lockTool,
|
|
954
|
+
unlockTool,
|
|
955
|
+
syncTool,
|
|
956
|
+
statusTool,
|
|
957
|
+
listTool,
|
|
958
|
+
getTool,
|
|
959
|
+
generateTool,
|
|
960
|
+
createTool,
|
|
961
|
+
editTool,
|
|
962
|
+
deleteTool,
|
|
963
|
+
],
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
const transport = new StdioServerTransport();
|
|
967
|
+
await server.connect(transport);
|
|
968
|
+
console.error('Bitwarden MCP Server running on stdio');
|
|
969
|
+
}
|
|
970
|
+
runServer().catch((error) => {
|
|
971
|
+
console.error('Fatal error running server:', error);
|
|
972
|
+
process.exit(1);
|
|
973
|
+
});
|