|
| 1 | +# WebAuthn MCP Tools Implementation Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Adding WebAuthn CDP domain support to chrome-devtools-mcp using strict outside-in behavior- and test-driven development. |
| 6 | + |
| 7 | +**Branch**: `feat/webauthn-support` |
| 8 | +**Fork**: `git@github.com:ed-lepedus-thenvoi/chrome-devtools-mcp.git` |
| 9 | + |
| 10 | +## Goal (Definition of Done) |
| 11 | + |
| 12 | +A user can: |
| 13 | +1. Enable WebAuthn virtual authenticator environment via MCP tool |
| 14 | +2. Add a virtual authenticator (CTAP2/U2F, internal/USB/BLE/NFC) |
| 15 | +3. Use WebAuthn on a webpage (e.g., webauthn.io) with the virtual authenticator responding |
| 16 | +4. Optionally: add pre-seeded credentials, get/remove credentials |
| 17 | + |
| 18 | +## Key Architecture Findings |
| 19 | + |
| 20 | +### How Tools Are Structured |
| 21 | + |
| 22 | +- **Tool Registry**: `src/tools/tools.ts` - exports all tools as array |
| 23 | +- **Tool Definition**: Use `defineTool()` helper from `src/tools/ToolDefinition.ts` |
| 24 | +- **Categories**: Defined in `src/tools/categories.ts` (use `EMULATION` for WebAuthn) |
| 25 | + |
| 26 | +### How to Access CDP Session |
| 27 | + |
| 28 | +```typescript |
| 29 | +const page = context.getSelectedPage(); |
| 30 | +const session = page._client() as CDPSession; |
| 31 | +await session.send('WebAuthn.enable'); |
| 32 | +``` |
| 33 | + |
| 34 | +This pattern is used in `src/PageCollector.ts` for `Audits.enable`. |
| 35 | + |
| 36 | +### Test Pattern |
| 37 | + |
| 38 | +Tests use `withMcpContext()` helper from `tests/utils.ts`: |
| 39 | + |
| 40 | +```typescript |
| 41 | +import {describe, it} from 'node:test'; |
| 42 | +import assert from 'node:assert'; |
| 43 | +import {withMcpContext} from '../utils.js'; |
| 44 | +import {enableWebAuthn} from '../../src/tools/webauthn.js'; |
| 45 | + |
| 46 | +describe('webauthn', () => { |
| 47 | + it('enables WebAuthn CDP domain', async () => { |
| 48 | + await withMcpContext(async (response, context) => { |
| 49 | + await enableWebAuthn.handler({params: {}}, response, context); |
| 50 | + // Verify by checking response or trying CDP operations |
| 51 | + }); |
| 52 | + }); |
| 53 | +}); |
| 54 | +``` |
| 55 | + |
| 56 | +### WebAuthn CDP Commands Available |
| 57 | + |
| 58 | +- `WebAuthn.enable` / `WebAuthn.disable` |
| 59 | +- `WebAuthn.addVirtualAuthenticator` → returns `{authenticatorId: string}` |
| 60 | +- `WebAuthn.removeVirtualAuthenticator` |
| 61 | +- `WebAuthn.addCredential` |
| 62 | +- `WebAuthn.getCredentials` |
| 63 | +- `WebAuthn.removeCredential` |
| 64 | +- `WebAuthn.clearCredentials` |
| 65 | +- `WebAuthn.setUserVerified` |
| 66 | + |
| 67 | +## Implementation Steps (Outside-In TDD) |
| 68 | + |
| 69 | +### Phase 1: Minimal Vertical Slice |
| 70 | + |
| 71 | +#### Step 1.1: Observe Missing Functionality |
| 72 | +- [x] Verify no `webauthn_*` tools exist in MCP |
| 73 | +- [x] Navigate to webauthn.io, confirm we can't do WebAuthn without virtual authenticator |
| 74 | + |
| 75 | +#### Step 1.2: Failing Test - Tool Exists |
| 76 | +Create `tests/tools/webauthn.test.ts`: |
| 77 | +```typescript |
| 78 | +it('webauthn_enable tool can be called') |
| 79 | +``` |
| 80 | +Run: `npm run test -- --test-name-pattern="webauthn"` |
| 81 | +Expected: FAIL (module not found) |
| 82 | + |
| 83 | +#### Step 1.3: Implement - Minimal Tool Skeleton |
| 84 | +- Create `src/tools/webauthn.ts` with `enableWebAuthn` tool (no-op handler) |
| 85 | +- Export from `src/tools/tools.ts` |
| 86 | +- Run test: Should PASS |
| 87 | +- Commit: `feat(webauthn): add webauthn_enable tool skeleton` |
| 88 | + |
| 89 | +#### Step 1.4: Verify Tool Appears in MCP |
| 90 | +- Rebuild: `npm run build` |
| 91 | +- Check if MCP picks up changes (may need restart) |
| 92 | +- Verify tool appears |
| 93 | + |
| 94 | +#### Step 1.5: Failing Test - Enable Actually Works |
| 95 | +```typescript |
| 96 | +it('enables WebAuthn so addVirtualAuthenticator succeeds', async () => { |
| 97 | + await withMcpContext(async (response, context) => { |
| 98 | + await enableWebAuthn.handler({params: {}}, response, context); |
| 99 | + const session = context.getSelectedPage()._client(); |
| 100 | + // This should succeed only if WebAuthn.enable was called |
| 101 | + const result = await session.send('WebAuthn.addVirtualAuthenticator', { |
| 102 | + options: { protocol: 'ctap2', transport: 'internal' } |
| 103 | + }); |
| 104 | + assert.ok(result.authenticatorId); |
| 105 | + }); |
| 106 | +}); |
| 107 | +``` |
| 108 | +Run: FAIL (WebAuthn not enabled) |
| 109 | + |
| 110 | +#### Step 1.6: Implement - CDP Call |
| 111 | +Add to handler: |
| 112 | +```typescript |
| 113 | +await context.getSelectedPage()._client().send('WebAuthn.enable'); |
| 114 | +``` |
| 115 | +Run test: PASS |
| 116 | +Commit: `feat(webauthn): implement WebAuthn.enable CDP call` |
| 117 | + |
| 118 | +#### Step 1.7: Verify via MCP |
| 119 | +- Call `webauthn_enable` tool |
| 120 | +- Confirm no error |
| 121 | + |
| 122 | +#### Step 1.8: Failing Test - Add Authenticator Tool |
| 123 | +```typescript |
| 124 | +it('adds virtual authenticator and returns ID') |
| 125 | +``` |
| 126 | +Run: FAIL (tool doesn't exist) |
| 127 | + |
| 128 | +#### Step 1.9: Implement - Add Authenticator |
| 129 | +- Add `addVirtualAuthenticator` tool with params: protocol, transport, hasResidentKey, hasUserVerification, isUserVerified |
| 130 | +- Run test: PASS |
| 131 | +- Commit: `feat(webauthn): add webauthn_add_authenticator tool` |
| 132 | + |
| 133 | +#### Step 1.10: E2E Verification |
| 134 | +1. Navigate to webauthn.io |
| 135 | +2. Call `webauthn_enable` |
| 136 | +3. Call `webauthn_add_authenticator` with ctap2/internal/userVerified |
| 137 | +4. Fill username, click Register |
| 138 | +5. Verify registration succeeds |
| 139 | + |
| 140 | +Commit: `test(webauthn): verify e2e with webauthn.io` |
| 141 | + |
| 142 | +### Phase 2: Expand Coverage |
| 143 | + |
| 144 | +After vertical slice works: |
| 145 | +- `webauthn_disable` |
| 146 | +- `webauthn_remove_authenticator` |
| 147 | +- `webauthn_get_credentials` |
| 148 | +- `webauthn_add_credential` |
| 149 | +- `webauthn_remove_credential` |
| 150 | +- `webauthn_clear_credentials` |
| 151 | +- `webauthn_set_user_verified` |
| 152 | + |
| 153 | +### Phase 3: Polish |
| 154 | +- Error handling tests |
| 155 | +- Run `npm run docs` to update documentation |
| 156 | +- Run `npm run check-format` and fix any issues |
| 157 | +- Full test suite pass |
| 158 | + |
| 159 | +## Local Development Setup |
| 160 | + |
| 161 | +```bash |
| 162 | +# MCP is configured to use local build: |
| 163 | +# claude mcp add-json chrome-devtools '{"command": "node", "args": ["/tmp/chrome-devtools-mcp-investigation/build/src/index.js"]}' |
| 164 | + |
| 165 | +# Build after changes: |
| 166 | +cd /tmp/chrome-devtools-mcp-investigation && npm run build |
| 167 | + |
| 168 | +# Run specific tests: |
| 169 | +npm run test -- --test-name-pattern="webauthn" |
| 170 | + |
| 171 | +# Run all tests: |
| 172 | +npm run test |
| 173 | + |
| 174 | +# Check formatting: |
| 175 | +npm run check-format |
| 176 | +``` |
| 177 | + |
| 178 | +## Notes |
| 179 | + |
| 180 | +- Node version: v24.9.0 (compatible) |
| 181 | +- Baseline: 288/288 tests passing |
| 182 | +- License header required on new files (see existing files for format) |
| 183 | +- MCP may need restart after rebuild to pick up changes (TBD - need to verify) |
| 184 | + |
| 185 | +## Files to Create/Modify |
| 186 | + |
| 187 | +1. **Create**: `src/tools/webauthn.ts` - Tool definitions |
| 188 | +2. **Modify**: `src/tools/tools.ts` - Add exports |
| 189 | +3. **Create**: `tests/tools/webauthn.test.ts` - Tests |
| 190 | + |
| 191 | +## Reference: Emulation Tool Pattern |
| 192 | + |
| 193 | +From `src/tools/emulation.ts`: |
| 194 | +```typescript |
| 195 | +export const emulate = defineTool({ |
| 196 | + name: 'emulate', |
| 197 | + description: '...', |
| 198 | + annotations: { |
| 199 | + category: ToolCategory.EMULATION, |
| 200 | + readOnlyHint: false, |
| 201 | + }, |
| 202 | + schema: { |
| 203 | + param1: zod.string().optional().describe('Description'), |
| 204 | + }, |
| 205 | + handler: async (request, response, context) => { |
| 206 | + const page = context.getSelectedPage(); |
| 207 | + // ... implementation |
| 208 | + response.appendResponseLine('Status message'); |
| 209 | + }, |
| 210 | +}); |
| 211 | +``` |
| 212 | + |
| 213 | +## Reference: Test Utilities |
| 214 | + |
| 215 | +From `tests/utils.ts`: |
| 216 | + |
| 217 | +- `withMcpContext(callback)` - Spawns browser, creates McpContext, calls callback |
| 218 | +- `McpResponse` - Mock response object with `appendResponseLine()`, etc. |
| 219 | +- Access CDP via: `context.getSelectedPage()._client()` returns CDPSession |
| 220 | + |
| 221 | +```typescript |
| 222 | +import assert from 'node:assert'; |
| 223 | +import {describe, it} from 'node:test'; |
| 224 | +import {withMcpContext} from '../utils.js'; |
| 225 | +import {myTool} from '../../src/tools/myTool.js'; |
| 226 | + |
| 227 | +describe('myTool', () => { |
| 228 | + it('does something', async () => { |
| 229 | + await withMcpContext(async (response, context) => { |
| 230 | + await myTool.handler({params: {...}}, response, context); |
| 231 | + // Assert on response or context state |
| 232 | + }); |
| 233 | + }); |
| 234 | +}); |
| 235 | +``` |
| 236 | + |
| 237 | +## Progress Log |
| 238 | + |
| 239 | +Track each step completion here: |
| 240 | + |
| 241 | +- [ ] Step 1.1: Observe missing functionality |
| 242 | +- [ ] Step 1.2: Failing test - tool exists |
| 243 | +- [ ] Step 1.3: Implement tool skeleton |
| 244 | +- [ ] Step 1.4: Verify tool appears in MCP |
| 245 | +- [ ] Step 1.5: Failing test - enable works |
| 246 | +- [ ] Step 1.6: Implement CDP call |
| 247 | +- [ ] Step 1.7: Verify via MCP |
| 248 | +- [ ] Step 1.8: Failing test - add authenticator |
| 249 | +- [ ] Step 1.9: Implement add authenticator |
| 250 | +- [ ] Step 1.10: E2E verification |
0 commit comments