bitwarden / bitwarden/mcp-server
generate tool cannot emit special characters, and any false value fails with an unknown option
- Dominant language
- TypeScript
- Stars
- 254
- Forks
- 34
- Avg merge
- 20h 45m
- Merged PRs (30d)
- 1
Description
# `generate` tool cannot emit special characters, and any `false` value fails with an unknown option
## Summary
`handleGenerate` maps boolean options to CLI flags only in the **negative** direction, and the negative flags it emits do not exist in the Bitwarden CLI. This produces two distinct failures:
1. **`special: true` is silently a no-op.** No flag is emitted, so the CLI applies its default character set, which excludes special characters. There is currently no way to generate a password containing symbols through this tool.
2. **Any `false` value makes the command fail.** `--noUppercase`, `--noLowercase`, `--noNumbers` and `--noSpecial` are not valid CLI options, so the spawned process exits with `error: unknown option`.
The CLI uses an opt-in model for character classes; the handler was written against an opt-out model that the CLI has never had.
## Environment
- `@bitwarden/mcp-server` 2026.7.0 (via `npx -y @bitwarden/mcp-server`)
- `@bitwarden/cli` 2026.6.0
- Node.js 24.18.1
- Windows 11, Claude Desktop
## Reproduction
### Case 1 — `true` is ignored
Call the `generate` tool with:
```json
{ "length": 20, "uppercase": true, "lowercase": true, "number": true, "special": true }
```
Observed output, two consecutive runs:
```
wADYjh6zL5wi5RyId90Q
Tjq1mJrsAMUE8M4yQ4hF
```
No special characters in either. The command actually executed is `bw generate --length 20`, which reproduces identically in a shell:
```
PS> bw generate --length 20
RHV2XjbMa8fNJaOpcqTW
```
For contrast, the flag the tool should have emitted does work:
```
PS> bw generate --length 20 --special
715*&@05^&48^38@49!&
```
### Case 2 — `false` errors
Call the `generate` tool with:
```json
{ "length": 20, "special": false }
```
The tool call fails and returns:
```
error: unknown option '--noSpecial'
(Did you mean one of --minSpecial, --special?)
```
This reproduces directly in a shell, confirming the flag does not exist:
```
PS> bw generate --length 20 --noSpecial
error: unknown option '--noSpecial'
(Did you mean one of --minSpecial, --special?)
```
The same applies to `--noUppercase`, `--noLowercase` and `--noNumbers`.
## Root cause
`src/handlers/cli.ts`, `handleGenerate`:
```ts
} else {
if (validatedArgs.length) {
params.push('--length', validatedArgs.length.toString());
}
if (validatedArgs.uppercase === false) {
params.push('--noUppercase');
}
if (validatedArgs.lowercase === false) {
params.push('--noLowercase');
}
if (validatedArgs.number === false) {
params.push('--noNumbers');
}
if (validatedArgs.special === false) {
params.push('--noSpecial');
}
}
```
Only the `=== false` branch is handled, and the flags it emits do not exist.
### The CLI's actual contract
`apps/cli/src/program.ts` registers `generate` with opt-in flags only — there are no negative variants:
```ts
program
.command("generate")
.description("Generate a password/passphrase.")
.option("-u, --uppercase", "Include uppercase characters.")
.option("-l, --lowercase", "Include lowercase characters.")
.option("-n, --number", "Include numeric characters.")
.option("-s, --special", "Include special characters.")
...
.on("--help", () => {
writeLn(" Default options are `-uln --length 14`.");
});
```
`apps/cli/src/tools/generate.command.ts` falls back to `-uln` only when no class is selected:
```ts
if (!this.uppercase && !this.lowercase && !this.special && !this.number) {
this.lowercase = true;
this.uppercase = true;
this.number = true;
}
```
The public documentation states the same:
> By default, the `generate` command will generate a 14-character password with uppercase characters, lowercase characters, and numbers. This is the equivalent of passing: `bw generate -uln --length 14`
—
### The schema already documents the correct intent
`src/schemas/cli.ts`:
```ts
// Include special characters in the password
special: z.boolean().optional(),
```
So the schema is opt-in and the handler is opt-out — they contradict each other.
## Impact
- Passwords generated through the MCP server never contain symbols, regardless of what the assistant requests. The tool reports success and returns a plausible-looking password, so the failure is **silent**: a user who asks for a symbol-containing password receives a weaker one with no indication that the request was dropped. For a password manager this seems worth treating as more than cosmetic.
- Explicitly disabling a character class fails the tool call outright.
## Proposed fix
Map the positive case to the CLI's opt-in flags:
```ts
} else {
if (validatedArgs.length) {
params.push('--length', validatedArgs.length.toString());
}
if (validatedArgs.uppercase) {
params.push('--uppercase');
}
if (validatedArgs.lowercase) {
params.push('--lowercase');
}
if (validatedArgs.number) {
params.push('--number');
}
if (validatedArgs.special) {
params.push('--special');
}
}
```
### Two behaviours worth deciding on explicitly
**Enabling one class does not simply add it to the default set.** With the patch, `{ "special": true }` alone sends `bw generate --special`, and letters drop out:
```
PS> bw generate --length 20 --special
715*&@05^&48^38@49!&
```
So callers need to enable every class they want, not just the one they are adding. Stating that in the tool description would help, since an LLM caller may reasonably assume the flags are additive to a sensible default. (Digits appear above without `--number` being passed; that is upstream CLI behaviour and out of scope here.)
**All-`false` is indistinguishable from unspecified.** With the patch, passing every class as `false` emits no flags, and the CLI's fallback silently produces a `-uln` password rather than rejecting the request. A `.refine()` on `generateSchema` requiring at least one class when `passphrase` is not set would surface this as a validation error instead.
### Suggested tests
- `special: true` produces `--special` in the argument list
- `uppercase: false` (and the other three) produce no `--no*` flag
- all four classes enabled produces all four flags
- passphrase branch is unchanged
## Note
The passphrase branch is unaffected — `--passphrase`, `--words`, `--separator` and `--capitalize` are already mapped correctly as opt-in flags.
## References
- CLI command registration: [`[apps/cli/src/program.ts](https://github.com/bitwarden/clients/blob/main/apps/cli/src/program.ts)`](https://github.com/bitwarden/clients/blob/main/apps/cli/src/program.ts)
- CLI default fallback: [`[apps/cli/src/tools/generate.command.ts](https://github.com/bitwarden/clients/blob/main/apps/cli/src/tools/generate.command.ts)`](https://github.com/bitwarden/clients/blob/main/apps/cli/src/tools/generate.command.ts)
- Documentation:
Contributor guide
Research direction
Start in src/handlers/cli.ts at handleGenerate, then compare its arguments with the generate registration in apps/cli/src/program.ts and fallback behavior in apps/cli/src/tools/generate.command.ts. Add or update tests covering positive character-class flags, omitted negative flags, all classes enabled, and the unchanged passphrase branch; done means these cases produce valid CLI arguments.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100