feat: improve MCPPairingPanel error messages for invalid token/port (#7)#17
feat: improve MCPPairingPanel error messages for invalid token/port (#7)#17iMindCap wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the MCPPairingPanel component and its corresponding unit tests to handle MCP pairing via URL hash parameters. It validates the token and port, saves successful pairings to Chrome local storage, and displays error messages with retry capabilities. The review feedback recommends refactoring the pairing logic into a shared helper function to eliminate code duplication between the initial load and retry handler, while also safely checking for the global chrome object to avoid runtime errors in non-extension environments.
| useEffect(() => { | ||
| const hash = window.location.hash; | ||
| const found = validatePairingParams(hash); | ||
|
|
||
| if (found.length === 0) { | ||
| const { token, port } = parseHash(hash); | ||
| chrome.storage?.local?.set({ mcpToken: token, mcpPort: port }); | ||
| setPaired(true); | ||
| } else { | ||
| setErrors(found); | ||
| } | ||
| }, []); | ||
|
|
||
| const handleRetry = () => { | ||
| setErrors([]); | ||
| setPaired(false); | ||
| const found = validatePairingParams(window.location.hash); | ||
| if (found.length === 0) { | ||
| const { token, port } = parseHash(window.location.hash); | ||
| chrome.storage?.local?.set({ mcpToken: token, mcpPort: port }); | ||
| setPaired(true); | ||
| } else { | ||
| setErrors(found); | ||
| } | ||
| }; |
There was a problem hiding this comment.
There is code duplication between the useEffect hook and the handleRetry handler. Additionally, directly accessing the global chrome object without checking if it is defined (e.g., typeof chrome !== 'undefined') can lead to a ReferenceError in non-extension environments (such as unit tests or standard browser tabs). Extracting this logic into a single helper function resolves both issues.
const performPairing = () => {
const hash = window.location.hash;
const found = validatePairingParams(hash);
if (found.length === 0) {
const { token, port } = parseHash(hash);
if (typeof chrome !== "undefined" && chrome?.storage?.local) {
chrome.storage.local.set({ mcpToken: token, mcpPort: port });
}
setPaired(true);
setErrors([]);
} else {
setErrors(found);
setPaired(false);
}
};
useEffect(() => {
performPairing();
}, []);
const handleRetry = () => {
performPairing();
};
hoainho
left a comment
There was a problem hiding this comment.
Thanks @iMindCap — really nice first-PR work. The validator regex is clean (anchored, no ReDoS surface), all 5 acceptance-criteria scenarios from #7 are tested, and the discriminated ErrorCode union is a sharp design choice.
I ran a multi-agent review (security, code quality, best-practices) and found 5 issues that should be fixed before merge. They're all subtle — not careless. Most come from a part of the MCP threat model that issue #7 didn't fully convey; that's on us, not you.
🔴 Critical (block merge)
C-1. Storage schema doesn't match what the rest of MCP v1 expects.
You write chrome.storage.local.set({ mcpToken: token, mcpPort: port }) — flat keys. The OpenSpec at specs/extension-mcp-client/spec.md (search for "Extension SHALL handle MCP pairing deep-link") requires:
await chrome.storage.local.set({
'mcp.pairing': { port: Number(port), token, pairedAt: Date.now() }
});The Service Worker MCP client (task 2.11) will look up chrome.storage.local.get('mcp.pairing') and find undefined — pairing will silently fail downstream. Existing repo convention matches: see src/background/index.ts#L56 (debugger_enabled_${tabId}) and src/services/ai-client.ts#L232 (ai_config).
C-2. Token persisted in browser history (security).
After a successful pair, window.location.hash still contains #token=<64-hex>&port=.... That token is now recoverable from browser history, session restore on crash, bookmarks, and the DevTools URL pane. Fix: call window.history.replaceState(null, '', window.location.pathname) right after the storage write resolves. (Violates OWASP ASVS 3.1.1 — session tokens must not live in URL state.)
C-3. No Origin check — anyone who can set the hash can silently re-pair you.
useEffect(() => performPairing(), []) auto-fires on every mount, regardless of who navigated here. A malicious page that can navigate the options page with a crafted hash could silently overwrite an active pairing with attacker-controlled values. Fix:
- Verify
location.protocol === 'chrome-extension:'andlocation.host === chrome.runtime.idbefore processing. - Require an explicit user click to confirm pairing — don't auto-write on mount.
- If existing pairing is present, prompt "Replace existing pairing with bridge on port X?" before overwriting.
C-4. Component is unreachable — options_page missing from manifest.
Verified directly: public/manifest.json has no options_page field and public/options.html doesn't exist. The component lives at src/panel/components/MCPPairingPanel.tsx but there's no host page to mount it. Either bundle the manifest change + options.html in this PR, or explicitly mark this PR "blocked by #X" and wait for the manifest wiring.
C-5. chrome.storage.local.set() errors silently swallowed.
The write is fire-and-forget. If it fails (quota exceeded, permissions error), the UI still says "MCP pairing successful!" — false positive. Per Chrome docs, wrap in await + try/catch:
try {
await chrome.storage.local.set({ 'mcp.pairing': {...} });
setPaired(true);
} catch (err) {
setErrors([{ code: 'storage-failed', message: 'Could not save pairing.', fix: err.message }]);
}🟡 Warnings (please also fix)
- Port validation accepts decimals/hex/whitespace.
Number('1024.5')→ 1024.5,Number('0x2000')→ 8192,Number(' 8080 ')→ 8080 all pass your range check. Add/^\d+$/.test(port)first. - Test file in wrong directory + wrong extension. Repo convention (verified: 11 existing tests) is
src/__tests__/*.test.tsx. Move tosrc/__tests__/MCPPairingPanel.test.tsx. - Inline styles violate the repo's CSS architecture. All existing components use class names from
src/panel/styles/panel.css;style={}is reserved for dynamic computed values only. Move structural styles to new classes like.mcp-pairing-panel. React.FCis a repo-wide antipattern (zero existing usage insrc/panel/**). Useexport function MCPPairingPanel(): React.JSX.Element { ... }with an explicit props interface.import React, { ... } from 'react'is redundant under"jsx": "react-jsx"(tsconfig). Drop the default import;noUnusedLocalswill error onceReact.FCis removed.
💡 Improvements (nice-to-have)
- Extract
validatePairingParams+parseHashtosrc/utils/validate-mcp-pairing.tsso tests don't need to import a React component file. Matches thesanitize.ts↔sanitize.test.tspattern. - Add a
useRefguard around the useEffect so React 18 StrictMode's double-mount doesn't cause issues. - Add edge-case tests: decimal/hex/leading-zero ports, duplicate hash keys, URL-encoded tokens, missing
#. - Trailing newline on both new files (POSIX).
What you got right
✅ All 5 issue-#7 acceptance-criteria scenarios tested · ✅ Regex ^[0-9a-fA-F]{64}$ correctly anchored — no ReDoS · ✅ Pure validator function is easy to test · ✅ Conventional Commits title · ✅ Closes #7 reference
Recommended next steps
- Read the extension-mcp-client spec — this is your integration contract.
- Address C-1 through C-5 first (security + integration before polish).
- Then the warnings.
- Push to the same branch — CI will re-run.
- Ping me (
@hoainho) for re-review.
Full review report on disk at .opencode/reviews/PR_17_2026-05-31.md — read if you want more depth on any finding.
Genuinely good first contribution. Don't be discouraged by the issue count — the bugs are non-obvious and you wrote the parts that are hardest (clean validator design, good tests, good error UX).
|
👋 Hey @iMindCap — checking in. No pressure at all, just wanted to make sure the review wasn't overwhelming. The 5 critical issues are a lot to process for a first PR, and most of them come from a part of the MCP threat model that issue #7 didn't fully explain. If any of the findings are unclear, happy to:
What works for you? Take your time — there's no SLA on first PRs. |
|
Hi @hoainho Thank you for the thorough review and for being so encouraging it really helps as a first-time contributor. |
|
Awesome @iMindCap — that's exactly the right call. Green light on both fronts: ✅ Confirmed planThis PR (now)Keep scope to: token format validator + error taxonomy + the test directory/inline-styles/React.FC cleanups. Those are independent of the MCP integration so they're safe to land first. Follow-up PRs (later)
I'll create those tracking issues this week so they're queued up. No work needed from you on them right now — just 🛠 What to do hereWhen you push the cleanup commits:
After those 3, request another review and I'll re-check + merge. 👏 Re first-time-contributor noteYou're handling this exceptionally well for a first PR. Most experienced contributors don't read a 5-finding security review this carefully. The scope-discipline alone (asking which path to take instead of guessing) is something most maintainers wish for. Take your time — there's no rush. — Hoài |
|
@iMindCap — quick follow-up: the 5 tracking issues for C-1 through C-5 are now open. Reference these in your PR description (or in follow-up PRs after this one merges):
All 5 are labeled Take whatever time you need on the cleanup commits in this PR. No deadline. 🙏 |
|
Hi @hoainho! I've addressed all 3 cleanups: Moved the test file to src/tests/MCPPairingPanel.test.tsx Also took the opportunity to fix the port validation to reject decimals, hex, and whitespace as you noted in the warnings. |
…w) (#48) Promotes the 'star the repo' rule from soft enforcement to a CI-blocking gate on PR merges. PRs from contributors who haven't starred the repo cannot merge until they do (single click + Re-run job). Mechanism: - New workflow .github/workflows/star-check.yml runs on every PR (opened, reopened, synchronize, ready_for_review, labeled, unlabeled). - Uses the public GitHub REST API 'GET /users/{login}/starred/{owner}/{repo}' via github-script. Returns 204 if starred, 404 if not. - No extra auth scope required beyond the default GITHUB_TOKEN. - Single job, 2 steps: the check + a summary write to the Actions UI. Auto-exemptions (no human action needed): - Maintainer PRs (@hoainho): hard-coded in the workflow's MAINTAINERS list. - Bot PRs: matches *[bot] suffix + an allowlist (dependabot, gemini-code-assist, google-cla, github-actions, renovate). - PRs labeled 'tracked-plan': for maintainer-driven milestone work (M-A, M-B, future Self-Roadmap milestones). - PRs labeled 'pre-star-rule': grandfathered PRs that were open before this policy landed (2026-06-01). Grandfathering (applied before this commit, separately via gh CLI): - 4 in-flight contributor PRs (#17, #36, #37, #38) labeled 'pre-star-rule' so the new check skips them. - 2 in-flight maintainer PRs (#39 M-A, #41 M-B) labeled 'tracked-plan' so the new check skips them. Failure UX: - When star check fails, the workflow writes a clear error message with: 'star the repo', 'click Re-run failed jobs', and a link to CONTRIBUTING.md. No re-push required to re-run after starring. Doc surface updated: - CONTRIBUTING.md: 'How to claim' section now flags the hard gate with a clear⚠️ notice; new 'Exemptions' subsection documents the 4 bypass categories. - PULL_REQUEST_TEMPLATE.md: 'Claim confirmation' section updated to note the CI enforcement. - CHANGELOG.md: new Unreleased entries under Added (workflow) + Changed (policy hardening) + Migration (grandfathering). Privacy + safety notes: - The API endpoint is public + read-only (no PII beyond GitHub's own public star list). - Workflow permissions are minimal: 'contents: read' + 'pull-requests: read'. No write access requested. - The check is idempotent and safe to re-run. Self-referential note: this PR itself will trigger the new workflow once merged. Author is @hoainho (in MAINTAINERS), so it auto-passes.
hoainho
left a comment
There was a problem hiding this comment.
@iMindCap Thanks for the careful refactor — 31645af addresses all 3 cleanups from the previous review cleanly:
✅ Fixed correctly:
React.FC→ function declaration- Inline styles →
.pairing-panel-container/.pairing-panel-error/.pairing-panel-error-fixCSS classes - Stricter port validation:
/^\\d+$/regex rejects decimals, hex, leading spaces - New tests in
src/__tests__/MCPPairingPanel.test.tsxcover the new regex edge cases (decimals, hex, leading spaces) — nice extension
⛔ One blocking issue:
The OLD test file src/panel/components/MCPPairingPanel.test.ts was NOT deleted when you moved the tests to src/__tests__/MCPPairingPanel.test.tsx. Both files now exist on the branch:
src/panel/components/MCPPairingPanel.test.ts (old, 5 cases)
src/__tests__/MCPPairingPanel.test.tsx (new, 8 cases — superset)
The new file is a strict superset of the old (same 5 cases + 3 new edge cases). Vitest will discover both and run all 13 tests = duplicate runs of 5 identical cases.
To fix: git rm src/panel/components/MCPPairingPanel.test.ts && git commit -m "refactor: remove duplicate test file (moved to src/__tests__/)" && git push
Once that's in, this PR is approved + ready to merge. Everything else looks great.
|
Hi @hoainho! All done: Removed the duplicate test file with git rm Ready for re-review whenever you get a chance! 🙏 |
Closes #7
Changes
MCPPairingPanel.tsxwith three distinct error messages:token-malformed: when token is not 64 hex charactersport-out-of-range: when port is outside 1024–65535missing-params: when both params are absentTests
All Vitest tests passing ✅