@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/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
+ });