Jgalea patch 1 - #4
Conversation
- Update package.json version - Update Dockerfile version label - Update MCP server version in source code - Automated version bump for release
- Add dedicated Docs API client with connection pooling - Add site and collection resolver utilities - Add comprehensive Docs API types and schemas - Support for Basic Auth with Docs API key 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add 42 Docs API endpoints with 100% coverage - Support all article, collection, category, and site operations - Add natural language query support for finding content - Implement article creation, update, and deletion - Add redirects management and site restrictions - Include proper response unwrapping and error handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add dedicated Reports API client for proper response unwrapping - Implement all report endpoints (company, email, chat, phone, user, happiness) - Fix happiness report endpoint URL to use /v2/reports/happiness/overall - Add comprehensive error handling and troubleshooting guidance - Support date ranges, comparisons, and filtering options 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add Reports API client to service container - Fix registerFactory to properly cache singleton instances - Add Docs API configuration (API key, default site ID) - Support delete safety flag for Docs operations - Add comprehensive cache clearing methods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add DocsToolHandler and ReportsToolHandler to tools registry - Add DocsResourceHandler to resources registry - Update tool routing to handle new endpoints - Maintain backwards compatibility with existing tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add comprehensive Docs API documentation with all 42 endpoints - Add Reports API documentation with examples - Document new environment variables (HELPSCOUT_DOCS_API_KEY, etc.) - Add changelog for v1.3.0 with all fixes and features - Update package-lock.json for version bump - Update extension manifest to match new version 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add .claude directory with SuperClaude configuration - Add detailed Reports feature documentation - Include implementation notes and API examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Change happiness endpoint from /v2/reports/happiness/overall to /v2/reports/happiness - Fix Reports API client to properly check response type before using 'in' operator - Add better error messages for unexpected string responses 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Keep happiness endpoint as /v2/reports/happiness (confirmed correct) - Add new getHappinessRatings tool for /v2/reports/happiness/ratings endpoint - Fix rating enum values from "okay/bad" to "ok/not-good" to match API - Improve Reports API client to check response type before using 'in' operator - Add better error messages for string responses from API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add comprehensive step-by-step OAuth2 setup instructions - Clarify why Client Credentials flow is used (not Authorization Code) - Add detailed troubleshooting for Reports API "Unknown URL" errors - Include plan requirements and feature availability checks - Add debugging tips for API permission issues - Improve visibility of OAuth scope requirements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Update instructions to specify using https://example.com as redirect URL - Clarify that redirect URL is required by form but not used for server apps - Remove confusing "leave blank" instruction 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
All Reports API endpoints were using /v2/reports/* but the base URL already includes /v2/, causing the final URL to be /v2/v2/reports/* which returned "Unknown URL" errors. - Fixed all report endpoints to use /reports/* instead of /v2/reports/* - Tested happiness report endpoint with curl and it now works correctly - This fixes getCompanyReport, getEmailReport, getChatReport, getPhoneReport, getUserReport, getHappinessReport, getHappinessRatings, and getDocsReport 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
The calculateTimeRange method was using setDate() which doesn't handle large day offsets correctly. When subtracting 180 days from August 1st, it would calculate -179 which JavaScript misinterprets, resulting in future dates. Changed to use setTime() with millisecond calculation which properly handles any number of days in the past. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Incorporates drewburchfield/help-scout-mcp-server upstream changes: - OAuth2 Client Credentials only (PATs removed) - Concurrent auth dedup via authenticationPromise pattern - ServiceContainer dependency injection - Updated config env vars (APP_ID/APP_SECRET) - Simplified API constraints and error handling - Resource handlers return TextResourceContents Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…ReportsToolHandler - DocsToolHandler and ReportsToolHandler extend Injectable via ServiceContainer - Consolidated tool definitions and search routing - Simplified docs-tools and reports-tools into handler classes - structuredConversationFilter for ticket/assignee/folder/customer lookup - All-status parallel search as default behavior Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…icket summarization Adds opt-in transcript fetching to searchConversations, solving the N+1 API call problem for ticket summarization. A single call now returns conversations with inline message transcripts. - includeTranscripts: boolean flag to attach transcripts to search results - transcriptMaxMessages: cap messages per conversation (default 10) - Extracted shared buildTranscript() method used by both getThreads and searchConversations - Parallel thread fetching via Promise.allSettled with graceful degradation - Smart default: limit drops to 10 when transcripts enabled - Updated prompts with transcript workflow examples Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…ture - Updated all tests for new OAuth2-only auth, config env vars, ServiceContainer - Added tests for includeTranscripts: inline transcripts, default limit, message cap - Fixed ESM mocking patterns (jest.unstable_mockModule, jest.spyOn) - Fixed nock query matcher types - Updated prompt assertions for transcript workflow Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- Removed references to non-existent tools (comprehensiveConversationSearch, advancedConversationSearch) - Added all actual tools including structuredConversationFilter, expanded Docs tools - Fixed env vars: HELPSCOUT_APP_ID/APP_SECRET, REDACT_MESSAGE_CONTENT - Removed PAT references (no longer supported) - Added Response Modes section (slim/verbose/transcript) - Added includeTranscripts search examples - Replaced inline changelog with link to GitHub Releases Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Scoped npm package name, transferred repo to GravityKit org, updated all references including npx commands, GitHub URLs, and copyright. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
No Docker usage; deleted Dockerfile, docker-compose, Docker test scripts, Docker README workflow, and production workflow doc. Simplified CI to test (Node 18/20/22/24/25) + publish on version tags. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Updated error messages to reference HELPSCOUT_APP_ID/APP_SECRET, redaction messages to reference REDACT_MESSAGE_CONTENT, .env.example to current vars, mcp.json to current tools/prompts. Removed stale test_search_examples.md. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Rewrote manifest.json for manifest_version 0.3 with correct tools, prompts, env vars, and compatibility. Updated dxt-validation tests to auto-run mcpb:build when needed and match current manifest structure. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Fixed tool names, env vars, response mode examples, removed Docker references, updated npm/GitHub URLs to GravityKit org, replaced inline changelog with link to GitHub Releases. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
DXT (Desktop Extensions) has been renamed to MCPB (MCP Bundles). Renamed build-dxt.js → build-mcpb.js, dxt-validation.test.ts → mcpb-validation.test.ts, updated all references, replaced generated SVG icon with official Help Scout PNG, and updated description. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Major version bump for breaking changes (renamed package, removed PAT auth, removed old tools, renamed env vars). CI now uses OIDC trusted publishing with provenance instead of NPM_TOKEN secret. Fixed lint error (let → const). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Updated index.test.ts version check from 1.7.0 to 2.0.0, silenced logger console.error spam in cache tests, regenerated package-lock. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Multiple defense layers on the attachment download path: 1. Tighten the zod schema to reject conversationId / attachmentId values outside [A-Za-z0-9_-]+. These IDs are interpolated into the API URL and into the on-disk path; a malformed value can pivot the request to an unrelated endpoint or escape the temp directory. 2. Sanitize the leaf filename via path.basename() (drops directory components), then strip leading dots and any character outside [A-Za-z0-9._-]. Blocks "../../../etc/passwd" style payloads even if an LLM is prompt-injected via attachment metadata. 3. Resolve the boundary directory absolutely and verify the final filePath stays inside it before writing. 4. Tighten file modes: directory 0o700, file 0o600 — previously default umask 022 left customer attachments world-readable on shared hosts. The original code path-joined unsanitized input.filename and input.conversationId into /tmp/helpscout-attachments/<id>/<name> and trusted the filesystem to do the right thing.
Help Scout's 4xx and 422 responses can echo input data verbatim, including validation errors that reflect the original payload. Before this change, the entire upstream response body was forwarded into the MCP tool result under details.apiResponse / details.validationErrors, which means customer PII from a createReply or createConversation call flowed straight back into the LLM context (and into anywhere that context is later logged or exported). Adds a safeApiResponse() helper in both helpscout-client.ts and helpscout-docs-client.ts that whitelists only top-level `code` and a length-capped (200 char) `message`. All four error-propagation sites (main 422, main generic 4xx, docs 422, docs generic 4xx) now use it instead of passing through the raw body.
The previous clear(prefix) implementation iterated every entry in the
LRU and queued each one for deletion regardless of the prefix arg, so
calling clear('GET:/conversations') silently nuked the entire cache —
mailbox data, auth state, everything. The original code admitted as
much in a comment.
Adds a prefixIndex (Map<prefix, Set<hashedKey>>) maintained alongside
the LRU so clear(prefix) targets only the matching entries. Hooks the
LRU's disposeAfter callback to keep the index in sync when entries are
evicted by the LRU itself (TTL expiry, capacity).
Also strips dynamic ID segments from cache prefix log lines: shapes
like 'GET:/conversations/12345/threads' become
'GET:/conversations/:id/threads' so debug logs stop correlating
customer ticket IDs with cache activity.
Three changes to the API client logging:
1. URL fields in debug/error logs now drop the ?... portion. Help Scout
queries routinely include customer email addresses (?email=...) and
query syntax like ?query=email:"jane@bigcorp.com". A new
stripQueryString() helper is shared inline by both clients (main +
docs).
2. Removes responsePreview from the Reports API debug log. The old
field serialized the first 200 chars of every response body — which
for happiness/email/chat reports includes agent names, customer
emails, and rating comments. Replaced with shape-only fields
(responseKeys, hasReport, responseType).
3. Switches the three Math.random().toString(36).substring(7) request
ID call sites to crypto.randomBytes(8).toString('hex'). Today the
IDs are only used for log correlation, but Math.random isn't a
CSPRNG and shouldn't be assumed safe for any future auth/dedup use.
Sites updated: helpscout-client.ts (request interceptor + transformError
url), helpscout-docs-client.ts (same), reports-api-client.ts (debug
preview), tools/index.ts callTool() requestId.
closePool() now sets accessToken = null and tokenExpiresAt = 0 before returning. JS strings are immutable so the underlying buffer may persist briefly until GC, but the live reference is gone and any follow-up request will re-auth from scratch — limiting recovery from a process-memory dump captured after shutdown. Adds an opt-in LOG_TOKEN_ROTATIONS=true toggle that emits an info-level "OAuth2 token rotated" line carrying only `rotatedAt` (ISO timestamp) and `expiresInSeconds`. The token value itself is never logged.
Adds two opt-in allowlist env vars that gate the four conversation
write tools and the three Docs write tools:
- HELPSCOUT_WRITE_INBOX_ALLOWLIST: comma-separated mailbox IDs.
Enforced by createConversation, createReply, createNote, and
updateConversation. createConversation reads the mailbox from the
input directly. The other three first resolve the conversation's
mailboxId via GET /conversations/{id} (which is normally cached).
- HELPSCOUT_WRITE_DOCS_SITE_ALLOWLIST: comma-separated Docs site IDs.
Enforced by createDocsArticle (collection -> site lookup),
updateDocsArticle (article -> collection -> site), and
saveDocsArticleDraft (same).
Default-allow when unset (preserves existing behavior — the
backwards-compat path most users already rely on). When set, every
unlisted target gets a structured error result naming the configured
allowlist so the LLM can self-correct rather than retry blindly.
This caps the blast radius if an LLM gets prompt-injected: a single
compromised session can't spam every mailbox or publish to every Docs
site, only the explicitly allowed ones.
Test-side: every test that mocks ../utils/config.js now exposes the
four new exports (isWriteInboxAllowed, getWriteInboxAllowlist, and the
docs-site equivalents) as default-allow stubs.
dist/ is regenerated by `npm run build` (and by the prepare / prepublishOnly hooks for npm publish). Committing it alongside source invites trust drift between dist/ and src/, and every published npm tarball was built from a tree that contained the leaked .claude/settings.local.json before the upstream rotation. claude-desktop-config.json is the example config — paths in the file make it the obvious place for users to paste real OAuth2 secrets and then commit them. The example will live in README.md instead. Both paths are now in .gitignore. The mcpb extension build directory (helpscout-mcp-extension/build/) is also ignored — it's a side effect of `npm run mcpb:build` running during tests and shouldn't be tracked. The package.json `files` allowlist no longer references claude-desktop-config.json, so npm publish won't try to ship a non-existent file. Note for the README rewrite: include a Claude Desktop config snippet that uses placeholders like `your-app-id` / `your-app-secret` and reminds users not to commit the resulting file.
…NAMES Two small hardening tweaks: 1. SearchConversationsInputSchema.query now z.string().max(1024). Defends against runaway query strings that would hit Help Scout's URI length limit (414) or trigger retry-on-429 stalls if the API returns a transient error on a malformed query. 2. HELPSCOUT_HIDE_INBOX_NAMES=true env toggle for users whose inbox names are themselves sensitive. When set, the MCP instructions list inbox IDs only, never names. Inbox names land in the connect-time `instructions` payload that MCP clients log and sometimes export — the toggle gives operators an opt-out. Note for the README rewrite: document HELPSCOUT_HIDE_INBOX_NAMES under the env var reference table.
Flip HELPSCOUT_WRITE_INBOX_ALLOWLIST and HELPSCOUT_WRITE_DOCS_SITE_ALLOWLIST to fail-closed when unset. Prior behavior was default-allow for backwards compatibility, but for an LLM-driven session that posture is too generous — operators must explicitly enumerate write targets to enable any write tool. Updates the rejection messages so the LLM gets a clear suggestion for both states (env unset vs ID not in allowlist).
Detects hardcoded secrets and AI-tooling files at staging time. Catches the GravityKit-style leak (.claude/settings.local.json with credentials in the bash allowlist) before it can be committed. Patterns cover Help Scout creds, AWS keys, GitHub PATs, Slack tokens, JWTs, Stripe live keys, and generic SECRET/TOKEN/API_KEY assignments. Install with: npm run install-hooks (copies to .git/hooks/pre-commit). Bypass (NOT recommended) with: git commit --no-verify.
- Name: @gravitykit/help-scout-mcp → @jgalea/help-scout-mcp - private: true to block accidental npm publish - Author: Jean Galea; preserve attribution to upstream maintainers (Drew Burchfield, Zack Katz) in contributors[] - Repository / homepage / bugs URLs point to jgalea fork - Add install-hooks npm script
- README rewritten for the hardened fork: prerequisites, quick start, Keychain-based credential pattern with launcher script, full env var reference (including new write-allowlist defaults), tool tables, development setup, attribution to upstream. - SECURITY.md adds disclosure contact, scope, posture summary, and operator recommendations for production deployment.
Read-only proof of life that exercises OAuth2 token acquisition and three GET endpoints (mailboxes, conversations, reports). Reads credentials from macOS Keychain by default with env-var fallback. Never calls a write endpoint, never logs secrets, never writes to disk — safe to run repeatedly against a production tenant. Run with: npm run smoke
Skips the launcher-script pattern by reading HELPSCOUT_APP_ID and HELPSCOUT_APP_SECRET from the macOS Keychain directly at startup. Service names are overridable via HELPSCOUT_KEYCHAIN_ID_SERVICE and HELPSCOUT_KEYCHAIN_SECRET_SERVICE. Keychain takes precedence over env vars when enabled — env vars leak via process listings, Keychain values stay local. Fails fast on non-macOS platforms and when Keychain entries are missing, with a copy-pasteable `security add-generic-password` hint.
Removes the npm publish + GitHub release jobs (we are a private fork with package.json private:true). Replaces with three jobs: - test: matrix Node 18/20/22, runs type-check, lint, build, jest - audit: npm audit on production deps with --audit-level=moderate - secret-scan: stages every tracked file and runs the pre-commit hook
Keep a Changelog format. All 18 hardening commits by category (Security, Removed, Changed, Added).
8-step procedure for incident response or quarterly rotation. Pre-flight, zero-downtime swap, audit log review, history scrub, common gotchas.
Audit Low drewburchfield#5 flagged regex-based HTML reshaping as defense-in-depth brittle. The structural rewrites (<pre> → <div>, bare <code> class tagging, <p> unwrap) now run as cheerio DOM operations, so attribute quirks and malformed inputs can't trick the regex into reintroducing dangerous strings. The remaining text-level passes (\n → <br>, <br>-adjacency normalization around block elements, trailing-break trim) stay as regex — they operate on output we generate ourselves from a sanitize-html allowlist, so no untrusted attribute content reaches them. Behavior is identical to the regex-based version; the existing 10 formatReplyHtml tests pass unchanged. Adds 6 edge-case tests covering <p> attributes, literal angle brackets in text, nested <pre><code> with surrounding whitespace, existing class on <code>, whitespace-only input, and inline children inside <p>.
Logger now writes single-line JSON to stderr by default, compatible with Datadog / Better Stack / CloudWatch / journald JSON ingest. Schema: timestamp, level, msg, plus whatever structured context the call site provides (already structured today; just stop pretty- printing). Switched from console.error to process.stderr.write for atomic, unbuffered single-line output. Added LOG_FORMAT=text for local-dev human-readable output. PII redaction via redactArgs is unchanged — still applied unconditionally at info level. Renames the JSON field from "message" to "msg" to match the schema log aggregators expect.
Node 18 fails because the codebase uses File as a global, which only became one in Node 20 (Node 18 EOL'd April 2025 regardless). Dropped 18 from CI matrix and bumped engines.node to >=20. Also npm audit fix transitively bumped path-to-regexp + a few others under @anthropic-ai/mcpb to clear 4 prod-dep advisories. After this, npm audit --omit=dev --audit-level=moderate exits 0.
createReply, createNote, and createConversation derived a resource id from the resource-id response header and then unconditionally returned success: true. When Help Scout returns 2xx without a resource-id header, the id is null but the handler still reported the write as created successfully. Seen in production: a reply/note reported created with threadId null, but nothing existed on the conversation. A successful thread/conversation create returns the new id in the resource-id header, so a null id means the write is unconfirmed. Return success: false with isError and a message telling the caller to verify via getThreads/getConversation before assuming the write landed. The happy path (real resource-id) is unchanged. Drafts and sends both require a confirming id. updateConversation is unaffected: it uses PATCH/PUT with no resource-id and confirms via a non-throwing re-fetch. Adds tests covering confirmed writes and the unconfirmed-write guard.
Bumps transitive deps to patched versions (sanitize-html 2.17.5, axios 1.18.0, undici 7.28.0, fast-uri 3.1.2, qs 6.15.2, hono 4.12.26, ip-address 10.2.0). Lockfile-only change; all within existing semver ranges. npm audit --omit=dev --audit-level=moderate now clean. Build, type-check, lint, and all 351 tests pass.
fix: don't report success on unconfirmed write tools
WalkthroughThis PR converts the fork to OAuth2 app credentials, adds Docs and Reports support paths, hardens config and logging behavior, rewrites packaging and CI around MCPB and secret scanning, updates server instructions and resources, and expands tests, runbooks, and Help Scout API reference documentation. ChangesCore fork refresh
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/runbooks/credential-rotation.md (1)
107-120:⚠️ Potential issue | 🟠 MajorLine 109: Keychain cache guidance is misleading.
The suggestion that
security delete-generic-passwordfollowed by re-add clears process-level Keychain cache is inaccurate. The macOS Keychain is a persistent database; applications maintain separate in-memory caches. Deleting from Keychain does not clear an app's cached copy—only restarting the shell/process does. The current phrasing presents this as an alternative to restart, which contradicts your own primary guidance. Revise line 109 to remove thesecurity delete-generic-passwordoption and emphasize shell restart as the reliable solution.Line 111: Verify
closePool()code exists.The statement "The new code (post-Audit) zeroes tokens on
closePool()" requires confirmation. MCP's specification does not define a standardclosePool()method; this is an implementation-specific pattern used by some SDKs. Confirm thatsrc/index.tsor relevant cleanup hooks actually implement this behavior, or remove the claim as unverified.Line 112: Google Workspace mention warrants clarification.
The reference to "admin.google.com" in a Help Scout credential rotation runbook is unusual. Clarify whether this integration is expected in the target deployment environment, or if this note applies only to specific configurations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/runbooks/credential-rotation.md` around lines 107 - 120, The credential rotation runbook contains three accuracy issues that need revision. First, in the "Common gotchas" section around line 109, remove the `security delete-generic-password` suggestion as an alternative to shell restart, since it does not actually clear process-level Keychain caches; revise to emphasize that only restarting the shell reliably clears the cached credentials. Second, verify that the MCP implementation in src/index.ts actually implements the `closePool()` method that zeroes tokens on credential rotation, and either confirm this behavior with a reference or remove the claim if unverified. Third, clarify the mention of "admin.google.com" in the Help Scout runbook around line 112 by either explaining when this Google Workspace integration applies to the deployment, or removing the reference if it is outside the scope of this Help Scout-specific rotation process.src/utils/api-constraints.ts (1)
81-87:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInclude
statusesin status-presence detection.Line 85 currently checks only
status. If a caller providesstatuses, this still emits “defaults to all statuses,” which is incorrect guidance.Proposed fix
- const hasStatus = args.status && typeof args.status === 'string'; + const hasStatus = args.status && typeof args.status === 'string'; + const hasStatuses = Array.isArray(args.statuses) && args.statuses.length > 0; const hasQuery = args.query && typeof args.query === 'string'; const hasTag = args.tag && typeof args.tag === 'string'; - if ((hasQuery || hasTag) && !hasStatus) { + if ((hasQuery || hasTag) && !hasStatus && !hasStatuses) { suggestions.push('TIP: Searching without status defaults to all statuses. For keyword search, provide searchTerms parameter.'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/api-constraints.ts` around lines 81 - 87, The hasStatus constant only checks for the `args.status` property but does not account for the `args.statuses` property. Update the hasStatus variable assignment to also check if args.statuses exists and is an array or non-empty collection, so that the subsequent condition checking `(hasQuery || hasTag) && !hasStatus` correctly identifies when a user has provided status filtering via either parameter name and avoids emitting the incorrect default-to-all-statuses suggestion.src/index.ts (1)
241-250:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAlways attempt pool shutdown even if MCP server close fails.
If
this.server.close()rejects,helpScoutClient.closePool()is never attempted. That can leave sockets open and block clean process teardown.Suggested patch
async stop(): Promise<void> { - try { - // Close the MCP server - await this.server.close(); - - // Close HTTP connection pool - await helpScoutClient.closePool(); - - logger.info('Help Scout MCP Server stopped'); - } catch (error) { - logger.error('Error stopping server', { - error: error instanceof Error ? error.message : String(error) - }); - } + let stopError: unknown; + try { + await this.server.close(); + } catch (error) { + stopError = error; + } + + try { + await helpScoutClient.closePool(); + } catch (error) { + stopError ??= error; + } + + if (stopError) { + logger.error('Error stopping server', { + error: stopError instanceof Error ? stopError.message : String(stopError), + }); + return; + } + + logger.info('Help Scout MCP Server stopped'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/index.ts` around lines 241 - 250, The stop() method's try-catch structure prevents the helpScoutClient.closePool() call from executing if this.server.close() throws an error. Restructure the stop() method to ensure pool closure is always attempted regardless of whether the server close succeeds or fails. Use a finally block or separate error handling to guarantee that helpScoutClient.closePool() executes even when this.server.close() rejects, ensuring sockets are properly closed before the process terminates.
🟡 Minor comments (8)
src/utils/helpscout-docs-client.ts-517-523 (1)
517-523:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCache TTL comment says "24 hours" but value is 24 minutes.
The comment says
// 24 hours for collectionsbut 1440 seconds = 24 minutes, not 24 hours. If 24 hours is intended, use86400.private getDefaultCacheTtl(endpoint: string): number { if (endpoint.includes('/articles')) return 600; // 10 minutes for articles - if (endpoint.includes('/collections')) return 1440; // 24 hours for collections - if (endpoint.includes('/categories')) return 1440; // 24 hours for categories - if (endpoint.includes('/sites')) return 1440; // 24 hours for sites + if (endpoint.includes('/collections')) return 86400; // 24 hours for collections + if (endpoint.includes('/categories')) return 86400; // 24 hours for categories + if (endpoint.includes('/sites')) return 86400; // 24 hours for sites return 600; // Default 10 minutes }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/helpscout-docs-client.ts` around lines 517 - 523, The comment for the collections, categories, and sites endpoints in the getDefaultCacheTtl method states "24 hours" but the value 1440 represents 24 minutes (1440 seconds), not 24 hours. Either update the TTL value to 86400 (which equals 24 hours in seconds) if a full day cache duration is intended, or correct the comment to accurately reflect that 1440 seconds equals 24 minutes. Ensure all three endpoints that share this value are consistent with the chosen fix.src/utils/collection-resolver.ts-161-227 (1)
161-227:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winEmpty response leaves cache invalid, causing repeated fetch attempts.
Same issue as in
SiteResolver: when no sites are found (Line 183-186), the method returns early without updatinglastFetch, causing repeated API calls.if (!sitesResponse.items || sitesResponse.items.length === 0) { logger.warn('No sites found for collection resolver'); + this.lastFetch = now; // Prevent repeated fetch attempts return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/collection-resolver.ts` around lines 161 - 227, In the ensureDataLoaded method, when no sites are found and the function returns early (lines 183-186), the lastFetch timestamp is not updated. This leaves the cache invalid, causing the next call to think the cache has expired and triggering another API fetch attempt. Move the this.lastFetch = now assignment to occur before the early return when sitesResponse.items is empty or missing, ensuring the cache is marked as refreshed even when no sites are found.src/utils/site-resolver.ts-137-180 (1)
137-180:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winEmpty response leaves cache invalid, causing repeated fetch attempts.
When no sites are found (Line 159-162), the method returns early without updating
lastFetch. This means every subsequent call will re-attempt the API fetch, potentially hammering the API unnecessarily.if (!sitesResponse.items || sitesResponse.items.length === 0) { logger.warn('No sites found for site resolver'); + this.lastFetch = now; // Prevent repeated fetch attempts return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/site-resolver.ts` around lines 137 - 180, The ensureDataLoaded() method returns early when no sites are found without updating this.lastFetch, which means the cache validity check on subsequent calls will always fail and trigger another API fetch attempt. Update this.lastFetch = now before the early return statement in the empty response check (the condition testing if sitesResponse.items is empty or has zero length) to ensure the cache duration is properly tracked regardless of whether sites are actually found.src/tools/reports-tools.ts-772-888 (1)
772-888:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winError from docs report API call is logged but not propagated to the response.
The catch block at Lines 834-836 logs the error but then continues execution. This means if the API call fails for reasons other than "Unknown URL" (e.g., network error, auth failure), the error is swallowed and the response falls through to the generic "endpoint not found" message, losing diagnostic information.
Consider propagating the actual error when it's not the expected "endpoint not found" case:
try { response = await reportsApiClient.getReport<DocsReportResponse>(endpoint, params); } catch (error: unknown) { - logger.error('Failed to get docs report', { error: error instanceof Error ? error.message : String(error) }); + const message = error instanceof Error ? error.message : String(error); + logger.error('Failed to get docs report', { error: message }); + // Only treat as "endpoint not found" if it's clearly that case + if (!message.includes('endpoint not found') && !message.includes('Unknown URL')) { + throw error; // Re-throw unexpected errors + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools/reports-tools.ts` around lines 772 - 888, The inner try-catch block in the docs report handling section (catching errors from the reportsApiClient.getReport call) logs the error but does not re-throw it, causing execution to continue with a null response. This results in returning a generic "endpoint not found" error message instead of the actual error that occurred. To fix this, re-throw the error in the inner catch block after logging it so that the outer try-catch block can properly handle and return the actual error details to the caller.src/utils/cache.ts-68-72 (1)
68-72:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCache hit/miss logs are wrong for falsy cached values.
On Line 68,
if (value)marks valid cached values like0,false, or""as misses.Suggested fix
- if (value) { + if (value !== undefined) { logger.debug('Cache hit', { key, prefix: shapePrefix(prefix) }); } else { logger.debug('Cache miss', { key, prefix: shapePrefix(prefix) }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/cache.ts` around lines 68 - 72, The cache hit/miss determination in the conditional block is incorrectly using truthiness check on the value variable. This causes falsy cached values like 0, false, or empty strings to be logged as cache misses even though they were successfully retrieved. Replace the `if (value)` check with a check that properly distinguishes between a value that was found in the cache versus one that was not found, such as checking if the value is not undefined or using a cache existence check method, so that valid falsy values are correctly logged as cache hits.src/utils/mime.ts-15-15 (1)
15-15:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHandle additional valid ZIP signatures in MIME detection.
Line [15] only matches
PK\x03\x04. Empty/spanned ZIPs (PK\x05\x06,PK\x07\x08) will fall through toapplication/octet-stream, which can break downstream extension recovery when the filename has no extension.Suggested patch
- { mime: 'application/zip', check: (b) => b.length >= 4 && b[0] === 0x50 && b[1] === 0x4B && b[2] === 0x03 && b[3] === 0x04 }, + { + mime: 'application/zip', + check: (b) => + b.length >= 4 && + b[0] === 0x50 && + b[1] === 0x4B && + ( + (b[2] === 0x03 && b[3] === 0x04) || // local file header + (b[2] === 0x05 && b[3] === 0x06) || // empty archive EOCD + (b[2] === 0x07 && b[3] === 0x08) // spanned archive + ), + },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/mime.ts` at line 15, The ZIP MIME type detection in the check function at the application/zip entry only validates one ZIP signature (PK\x03\x04), causing empty and spanned ZIP files with signatures PK\x05\x06 and PK\x07\x08 to be misidentified as application/octet-stream. Update the check function for the application/zip entry to also validate these additional valid ZIP signatures (bytes 0x50, 0x4B, 0x05, 0x06 and 0x50, 0x4B, 0x07, 0x08) by extending the conditional logic to accept any of these three signature patterns, ensuring all valid ZIP files are correctly detected.src/schema/types.ts-307-330 (1)
307-330:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDocs update schemas permit no-op updates (ID only).
Lines 307-330 allow requests with only
articleId/collectionId/categoryId. That passes schema validation but produces avoidable upstream validation errors. Add local “at least one field to update” constraints, similar toUpdateConversationInputSchema.Suggested fix
-export const UpdateDocsArticleInputSchema = z.object({ +export const UpdateDocsArticleInputSchema = z.object({ articleId: z.string(), name: z.string().optional(), text: z.string().optional(), status: z.string().optional(), categories: z.array(z.string()).optional(), related: z.array(z.string()).optional(), -}); +}).refine( + (data) => !!(data.name !== undefined || data.text !== undefined || data.status !== undefined || data.categories !== undefined || data.related !== undefined), + { message: 'At least one article field to update must be provided.' } +); -export const UpdateDocsCollectionInputSchema = z.object({ +export const UpdateDocsCollectionInputSchema = z.object({ collectionId: z.string(), name: z.string().optional(), description: z.string().optional(), visibility: z.enum(['public', 'private']).optional(), order: z.number().optional(), -}); +}).refine( + (data) => !!(data.name !== undefined || data.description !== undefined || data.visibility !== undefined || data.order !== undefined), + { message: 'At least one collection field to update must be provided.' } +); -export const UpdateDocsCategoryInputSchema = z.object({ +export const UpdateDocsCategoryInputSchema = z.object({ categoryId: z.string(), name: z.string().optional(), description: z.string().optional(), visibility: z.enum(['public', 'private']).optional(), order: z.number().optional(), -}); +}).refine( + (data) => !!(data.name !== undefined || data.description !== undefined || data.visibility !== undefined || data.order !== undefined), + { message: 'At least one category field to update must be provided.' } +);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/schema/types.ts` around lines 307 - 330, The three update schemas (UpdateDocsArticleInputSchema, UpdateDocsCollectionInputSchema, and UpdateDocsCategoryInputSchema) currently allow requests with only the ID field and no other fields to update, which passes validation but causes upstream errors. Add a .refine() constraint to each of these three schemas to enforce that at least one field besides the ID is provided for update. For UpdateDocsArticleInputSchema, validate that at least one of name, text, status, categories, or related is provided. For UpdateDocsCollectionInputSchema, validate that at least one of name, description, visibility, or order is provided. For UpdateDocsCategoryInputSchema, validate that at least one of name, description, visibility, or order is provided. Reference UpdateConversationInputSchema as a pattern for implementing this validation constraint.src/__tests__/integration.test.ts-9-10 (1)
9-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign the integration config mock with current runtime contracts.
Line 9 falling back
clientIdtoHELPSCOUT_API_KEY, and Lines 34–36 honoringargs.verbose, can let tests pass under assumptions that diverge from intended OAuth2 + env-driven verbose behavior.Proposed fix
- get clientId() { return process.env.HELPSCOUT_APP_ID || process.env.HELPSCOUT_CLIENT_ID || process.env.HELPSCOUT_API_KEY || ''; }, + get clientId() { return process.env.HELPSCOUT_APP_ID || process.env.HELPSCOUT_CLIENT_ID || ''; }, @@ - isVerbose: (args: unknown) => { - if (args && typeof args === 'object' && 'verbose' in args && typeof (args as any).verbose === 'boolean') { - return (args as any).verbose; - } - return process.env.HELPSCOUT_VERBOSE_RESPONSES === 'true'; - }, + isVerbose: () => process.env.HELPSCOUT_VERBOSE_RESPONSES === 'true',As per coding guidelines,
src/schema/types.ts: keepverbosesupport as environment-driven behavior; do not add averbosetool parameter back into schemas unless intentionally reversing the current design.Also applies to: 33-38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/integration.test.ts` around lines 9 - 10, The integration test config mock in src/__tests__/integration.test.ts has two misalignments with the intended runtime behavior: First, remove the fallback to HELPSCOUT_API_KEY from the clientId getter (which should only fall back to HELPSCOUT_APP_ID or HELPSCOUT_CLIENT_ID) to align with the OAuth2 contract. Second, ensure that verbose behavior is driven only by environment variables and remove any handling of args.verbose from the mock configuration (around lines 33-38) to prevent tests from passing under incorrect assumptions about tool parameter handling.Source: Coding guidelines
🧹 Nitpick comments (3)
.github/workflows/ci.yml (1)
24-24: ⚡ Quick winConsider pinning actions to commit SHAs for supply-chain security.
Static analysis flags these action references as unpinned. While version tags (e.g.,
@v5) are common and Dependabot will help keep them updated, SHA-pinning provides stronger protection against tag hijacking. Addingpersist-credentials: falseto checkout steps is also a hardening measure.If you prefer to keep tag-based references for readability, the current setup is acceptable given the read-only permissions and Dependabot coverage.
🔒 Example SHA-pinned checkout with persist-credentials: false
- - uses: actions/checkout@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: falseNote: Look up the current SHA for the version you want to pin to.
Also applies to: 27-27, 44-44, 46-46, 59-59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml at line 24, Replace all the actions/checkout references that currently use version tags (such as `@v5`) with their corresponding commit SHAs for stronger supply-chain security protection. For each checkout action usage (lines 24, 27, 44, 46, and 59), update the uses statement to reference the full commit SHA instead of the version tag, and add persist-credentials: false as a parameter to each actions/checkout step to disable credential persistence as a hardening measure.Source: Linters/SAST tools
src/__tests__/api-constraints.test.ts (1)
63-75: ⚡ Quick winAdd tests for the newly introduced validator branches.
This file updates the
searchTermshint assertion, but the newvalidateToolCallbranches forcreateReply,getConversation,createConversation, andupdateConversationare still unverified here. Add direct assertions for required-field and numeric-ID validation to prevent regressions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/api-constraints.test.ts` around lines 63 - 75, The test file only validates the searchTerms suggestion logic but does not have test cases for the new validator branches that were added for createReply, getConversation, createConversation, and updateConversation. Add separate test cases for each of these tool calls within the same test file that verify the required-field validation and numeric-ID validation logic. For each new test, create a ToolCallContext with the appropriate toolName and arguments, call HelpScoutAPIConstraints.validateToolCall, and add specific assertions to verify that validation failures occur when required fields are missing and that numeric ID constraints are properly enforced.src/__tests__/index.test.ts (1)
302-330: ⚡ Quick winAssert
redactArgsis called in tool/prompt logging tests.Current assertions pass even if redaction is bypassed, because the mock returns arguments unchanged and call expectations only inspect logger payload shape.
Suggested patch
it('should handle tool calls with proper logging', async () => { const { toolHandler } = require('../tools/index.js'); - const { logger } = require('../utils/logger.js'); + const { logger, redactArgs } = require('../utils/logger.js'); @@ const result = await handler(request); @@ + expect(redactArgs).toHaveBeenCalledWith({ query: 'test' }); expect(logger.debug).toHaveBeenCalledWith('Calling tool', { name: 'searchInboxes', arguments: { query: 'test' } }); }); @@ it('should handle prompt requests with proper logging', async () => { const { promptHandler } = require('../prompts/index.js'); - const { logger } = require('../utils/logger.js'); + const { logger, redactArgs } = require('../utils/logger.js'); @@ const result = await handler(request); @@ + expect(redactArgs).toHaveBeenCalledWith({ inboxId: '123' }); expect(logger.debug).toHaveBeenCalledWith('Getting prompt', { name: 'search-last-7-days', arguments: { inboxId: '123' } }); });Also applies to: 356-384
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/index.test.ts` around lines 302 - 330, The test for handling tool calls with proper logging does not verify that the redactArgs function is actually being called to redact sensitive arguments. To fix this, mock the redactArgs function (similar to how toolHandler and logger are mocked) and add an expectation assertion to verify that redactArgs is called with the tool arguments before the logger.debug assertion. This ensures the test confirms that argument redaction is actually happening in the logging flow, not just that the logger is invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/runbooks/credential-rotation.md`:
- Around line 88-96: The credential rotation runbook uses bash process
substitution in the git filter-repo command which is not cross-platform
compatible, lacks installation instructions for git-filter-repo, and does not
clearly indicate that the hardcoded grep pattern example must be replaced with
an actual leaked secret. Add a prerequisites section before the grep command
with installation instructions for git-filter-repo (including minimum version
requirements and installation methods). Replace the hardcoded pattern
qCJgfjX8a34wTWgn7yWYHUBP6TGHcGFa in the grep example with a clear placeholder
comment stating operators must substitute their actual revoked secret. Replace
the process substitution syntax in the git filter-repo --replace-text command
with a file-based approach by creating a temporary replacement file, passing it
as an argument to git filter-repo, and then cleaning up the temporary file to
ensure the runbook works reliably across Linux, macOS, and Windows systems.
In `@helpscout-mcp-extension/manifest.json`:
- Around line 27-29: The Node runtime constraint in the manifest.json file under
the runtimes section is set to ">=18.0.0" but this is inconsistent with the
package.json requirement of ">=20.0.0" and the codebase uses globalThis.File
which is only available in Node 20 and above. Update the "node" value in the
"runtimes" object in manifest.json from ">=18.0.0" to ">=20.0.0" to match the
package.json constraint and ensure consistency across all configuration files.
In `@README.md`:
- Around line 23-24: Update all git clone commands in README.md that reference
the `jgalea/help-scout-mcp` repository to use the canonical repository URL
`GravityKit/help-scout-mcp` instead. This includes the clone command in the
setup instructions section and the one that also appears later in the document.
Ensure consistency across the README.md with the repository targeted by this PR
and with the CONTRIBUTING.md documentation.
In `@scripts/bump-version.cjs`:
- Line 94: The git add command in the execSync call is missing Dockerfile from
the list of files to stage, even though updateDockerfile(newVersion) modifies it
on line 136. Add Dockerfile to the git add command alongside the other
version-related files (package.json, src/index.ts, mcp.json,
helpscout-mcp-extension/manifest.json) to ensure the Dockerfile changes are
included in the version bump commit.
In `@scripts/pre-commit-secret-scan.sh`:
- Around line 47-51: Remove the exclusion patterns for test files in the git
diff command within the hits variable assignment. Currently, the patterns
':(exclude)*.test.ts', ':(exclude)*.test.js', and ':(exclude)__tests__/*' are
preventing test files from being scanned for secrets. Delete these exclusion
patterns from the git diff command so that all files, including test files, are
included in the secret scanning check.
In `@scripts/smoke-test.js`:
- Around line 103-113: The smoke-test script is logging customer-facing data
that may appear in CI logs and terminals. In the first console.log statement
printing mailbox information, remove the logged mailbox names (the names
variable) and only log the count of inboxes. In the second console.log statement
for the first conversation, remove the firstConv.subject from the output as it
contains potentially sensitive conversation data. Keep only non-sensitive
identifiers like the conversation ID if needed for diagnostic purposes.
In `@src/__tests__/connection-pool.test.ts`:
- Around line 8-10: The get clientId() getter in the auth mock still includes a
fallback to process.env.HELPSCOUT_API_KEY which is deprecated and no longer
reflects the actual runtime configuration behavior. Remove the fallback to
HELPSCOUT_API_KEY from the clientId getter chain so it only checks
HELPSCOUT_APP_ID and HELPSCOUT_CLIENT_ID in that order, matching the current
runtime behavior and preventing test regressions from masking OAuth2
authentication issues.
In `@src/__tests__/docs-tools.test.ts`:
- Around line 8-10: Remove the deprecated HELPSCOUT_API_KEY fallback from the
get clientId() method in the test auth mock configuration. Modify the clientId
getter to only check process.env.HELPSCOUT_APP_ID and
process.env.HELPSCOUT_CLIENT_ID in sequence, removing the HELPSCOUT_API_KEY
check that no longer matches the runtime configuration and could mask
authentication regressions during testing.
- Around line 34-39: The isVerbose function in the mock currently includes
argument-level verbose override logic that checks if args contains a verbose
property. Remove this conditional check that examines the args object and its
verbose property, keeping only the environment variable check that returns
whether process.env.HELPSCOUT_VERBOSE_RESPONSES equals 'true'. This ensures the
mock aligns with the environment-driven verbosity design pattern specified in
src/schema/types.ts.
In `@src/__tests__/helpscout-client.test.ts`:
- Around line 33-34: The mocked clientId getter in the test configuration
includes HELPSCOUT_API_KEY as a fallback option, but the production OAuth2
config does not support this fallback. Remove the HELPSCOUT_API_KEY fallback
from the clientId getter's resolution chain so that it only falls back to
HELPSCOUT_APP_ID and HELPSCOUT_CLIENT_ID in that order, matching the production
contract and preventing tests from passing with deprecated environment variable
paths.
In `@src/__tests__/tools.test.ts`:
- Around line 9-10: The clientId getter in the mock configuration is using
HELPSCOUT_API_KEY as a fallback, which diverges from the runtime configuration
and can mask authentication issues in tests. Remove the HELPSCOUT_API_KEY
fallback from the clientId getter on line 9, keeping only HELPSCOUT_APP_ID and
HELPSCOUT_CLIENT_ID as the fallback chain before the empty string default, so
the test mock aligns with actual runtime behavior.
- Around line 35-40: The isVerbose mock currently allows tool arguments to
override the environment-driven verbose behavior, which can mask regressions.
Simplify the isVerbose function to remove all logic that checks for the verbose
property in args (the entire conditional block checking args && typeof args ===
'object' && 'verbose' in args, etc.) and instead have the function only check
and return the result of the process.env.HELPSCOUT_VERBOSE_RESPONSES environment
variable check, making it purely environment-driven.
In `@src/index.ts`:
- Around line 91-102: The Tool Selection Guide in src/index.ts still references
the deprecated getHappinessRatings tool in the "Get report data" row, which has
been merged into the getReport endpoint. Remove the mention of
getHappinessRatings from the guide and update the "Get report data" row to only
reference getReport, ensuring the documentation is consistent with the actual
merged tool implementation defined in src/tools/reports-tools.ts and prevents
clients from making invalid tool calls.
In `@src/schema/types.ts`:
- Around line 136-153: In the StructuredConversationFilterInputSchema refine
validation logic, the check for customerIds uses only data.customerIds !==
undefined, which allows empty arrays to pass the "unique field" requirement.
Update the condition to verify that customerIds is not only defined but also has
a non-zero length (e.g., data.customerIds && data.customerIds.length > 0) so
that empty arrays do not satisfy the validation requirement and trigger the
appropriate error message.
In `@src/utils/cache.ts`:
- Around line 77-80: The set method in the cache utility is using a falsy check
on options?.ttl, which incorrectly treats the explicit value ttl: 0 as undefined
and falls back to the default TTL. Fix the TTL assignment logic on line 79 by
replacing the truthiness check (options?.ttl ?) with a proper nullish coalescing
check (options?.ttl !== undefined) so that callers who intentionally pass ttl: 0
to disable caching have that value respected instead of being overridden with
the default TTL.
In `@src/utils/helpscout-docs-client.ts`:
- Around line 590-616: The clearIdleConnections() method destroys and recreates
this.httpAgent and this.httpsAgent, but the this.client Axios instance still
holds references to the old destroyed agents from when it was originally
constructed. This causes subsequent requests to fail because the Axios instance
continues using the destroyed agents. After recreating the agents with the
poolConfig, you must also update the Axios instance to use these new agents by
either updating this.client's httpAgent and httpsAgent defaults or by recreating
the this.client instance entirely with the new agents.
In `@src/utils/service-container.ts`:
- Around line 220-236: The getService method in the Injectable class is ignoring
the injected container and always calling ServiceContainer.getInstance()
directly, which bypasses the custom resolver stored in this.services. Fix this
by changing the getService method to use the injected resolver
this.services.get(key) instead of ServiceContainer.getInstance().get(key), so
that tests using a custom container will receive their mocked services instead
of the global singleton.
---
Outside diff comments:
In `@docs/runbooks/credential-rotation.md`:
- Around line 107-120: The credential rotation runbook contains three accuracy
issues that need revision. First, in the "Common gotchas" section around line
109, remove the `security delete-generic-password` suggestion as an alternative
to shell restart, since it does not actually clear process-level Keychain
caches; revise to emphasize that only restarting the shell reliably clears the
cached credentials. Second, verify that the MCP implementation in src/index.ts
actually implements the `closePool()` method that zeroes tokens on credential
rotation, and either confirm this behavior with a reference or remove the claim
if unverified. Third, clarify the mention of "admin.google.com" in the Help
Scout runbook around line 112 by either explaining when this Google Workspace
integration applies to the deployment, or removing the reference if it is
outside the scope of this Help Scout-specific rotation process.
In `@src/index.ts`:
- Around line 241-250: The stop() method's try-catch structure prevents the
helpScoutClient.closePool() call from executing if this.server.close() throws an
error. Restructure the stop() method to ensure pool closure is always attempted
regardless of whether the server close succeeds or fails. Use a finally block or
separate error handling to guarantee that helpScoutClient.closePool() executes
even when this.server.close() rejects, ensuring sockets are properly closed
before the process terminates.
In `@src/utils/api-constraints.ts`:
- Around line 81-87: The hasStatus constant only checks for the `args.status`
property but does not account for the `args.statuses` property. Update the
hasStatus variable assignment to also check if args.statuses exists and is an
array or non-empty collection, so that the subsequent condition checking
`(hasQuery || hasTag) && !hasStatus` correctly identifies when a user has
provided status filtering via either parameter name and avoids emitting the
incorrect default-to-all-statuses suggestion.
---
Minor comments:
In `@src/__tests__/integration.test.ts`:
- Around line 9-10: The integration test config mock in
src/__tests__/integration.test.ts has two misalignments with the intended
runtime behavior: First, remove the fallback to HELPSCOUT_API_KEY from the
clientId getter (which should only fall back to HELPSCOUT_APP_ID or
HELPSCOUT_CLIENT_ID) to align with the OAuth2 contract. Second, ensure that
verbose behavior is driven only by environment variables and remove any handling
of args.verbose from the mock configuration (around lines 33-38) to prevent
tests from passing under incorrect assumptions about tool parameter handling.
In `@src/schema/types.ts`:
- Around line 307-330: The three update schemas (UpdateDocsArticleInputSchema,
UpdateDocsCollectionInputSchema, and UpdateDocsCategoryInputSchema) currently
allow requests with only the ID field and no other fields to update, which
passes validation but causes upstream errors. Add a .refine() constraint to each
of these three schemas to enforce that at least one field besides the ID is
provided for update. For UpdateDocsArticleInputSchema, validate that at least
one of name, text, status, categories, or related is provided. For
UpdateDocsCollectionInputSchema, validate that at least one of name,
description, visibility, or order is provided. For
UpdateDocsCategoryInputSchema, validate that at least one of name, description,
visibility, or order is provided. Reference UpdateConversationInputSchema as a
pattern for implementing this validation constraint.
In `@src/tools/reports-tools.ts`:
- Around line 772-888: The inner try-catch block in the docs report handling
section (catching errors from the reportsApiClient.getReport call) logs the
error but does not re-throw it, causing execution to continue with a null
response. This results in returning a generic "endpoint not found" error message
instead of the actual error that occurred. To fix this, re-throw the error in
the inner catch block after logging it so that the outer try-catch block can
properly handle and return the actual error details to the caller.
In `@src/utils/cache.ts`:
- Around line 68-72: The cache hit/miss determination in the conditional block
is incorrectly using truthiness check on the value variable. This causes falsy
cached values like 0, false, or empty strings to be logged as cache misses even
though they were successfully retrieved. Replace the `if (value)` check with a
check that properly distinguishes between a value that was found in the cache
versus one that was not found, such as checking if the value is not undefined or
using a cache existence check method, so that valid falsy values are correctly
logged as cache hits.
In `@src/utils/collection-resolver.ts`:
- Around line 161-227: In the ensureDataLoaded method, when no sites are found
and the function returns early (lines 183-186), the lastFetch timestamp is not
updated. This leaves the cache invalid, causing the next call to think the cache
has expired and triggering another API fetch attempt. Move the this.lastFetch =
now assignment to occur before the early return when sitesResponse.items is
empty or missing, ensuring the cache is marked as refreshed even when no sites
are found.
In `@src/utils/helpscout-docs-client.ts`:
- Around line 517-523: The comment for the collections, categories, and sites
endpoints in the getDefaultCacheTtl method states "24 hours" but the value 1440
represents 24 minutes (1440 seconds), not 24 hours. Either update the TTL value
to 86400 (which equals 24 hours in seconds) if a full day cache duration is
intended, or correct the comment to accurately reflect that 1440 seconds equals
24 minutes. Ensure all three endpoints that share this value are consistent with
the chosen fix.
In `@src/utils/mime.ts`:
- Line 15: The ZIP MIME type detection in the check function at the
application/zip entry only validates one ZIP signature (PK\x03\x04), causing
empty and spanned ZIP files with signatures PK\x05\x06 and PK\x07\x08 to be
misidentified as application/octet-stream. Update the check function for the
application/zip entry to also validate these additional valid ZIP signatures
(bytes 0x50, 0x4B, 0x05, 0x06 and 0x50, 0x4B, 0x07, 0x08) by extending the
conditional logic to accept any of these three signature patterns, ensuring all
valid ZIP files are correctly detected.
In `@src/utils/site-resolver.ts`:
- Around line 137-180: The ensureDataLoaded() method returns early when no sites
are found without updating this.lastFetch, which means the cache validity check
on subsequent calls will always fail and trigger another API fetch attempt.
Update this.lastFetch = now before the early return statement in the empty
response check (the condition testing if sitesResponse.items is empty or has
zero length) to ensure the cache duration is properly tracked regardless of
whether sites are actually found.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Line 24: Replace all the actions/checkout references that currently use
version tags (such as `@v5`) with their corresponding commit SHAs for stronger
supply-chain security protection. For each checkout action usage (lines 24, 27,
44, 46, and 59), update the uses statement to reference the full commit SHA
instead of the version tag, and add persist-credentials: false as a parameter to
each actions/checkout step to disable credential persistence as a hardening
measure.
In `@src/__tests__/api-constraints.test.ts`:
- Around line 63-75: The test file only validates the searchTerms suggestion
logic but does not have test cases for the new validator branches that were
added for createReply, getConversation, createConversation, and
updateConversation. Add separate test cases for each of these tool calls within
the same test file that verify the required-field validation and numeric-ID
validation logic. For each new test, create a ToolCallContext with the
appropriate toolName and arguments, call
HelpScoutAPIConstraints.validateToolCall, and add specific assertions to verify
that validation failures occur when required fields are missing and that numeric
ID constraints are properly enforced.
In `@src/__tests__/index.test.ts`:
- Around line 302-330: The test for handling tool calls with proper logging does
not verify that the redactArgs function is actually being called to redact
sensitive arguments. To fix this, mock the redactArgs function (similar to how
toolHandler and logger are mocked) and add an expectation assertion to verify
that redactArgs is called with the tool arguments before the logger.debug
assertion. This ensures the test confirms that argument redaction is actually
happening in the logging flow, not just that the logger is invoked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cc2865fe-2645-4a1a-93bc-4c77d090bb0d
⛔ Files ignored due to path filters (3)
helpscout-mcp-extension/icon.pngis excluded by!**/*.pnghelpscout-mcp-extension/icon.svgis excluded by!**/*.svgpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (76)
.env.example.eslintrc.json.github/dependabot.yml.github/workflows/ci.yml.github/workflows/update-docker-readme.yml.gitignore.semgrepignoreAGENTS.mdCHANGELOG.mdCONTRIBUTING.mdDockerfileLICENSEPRODUCTION_WORKFLOW.mdREADME.mdSECURITY.mdclaude-desktop-config.jsondocker-compose.ymldocs/help-scout-api-reference.mddocs/runbooks/credential-rotation.mdeslint.config.jshelp-scout-complete-api-documentation.mdhelpscout-mcp-extension/.gitignorehelpscout-mcp-extension/README.mdhelpscout-mcp-extension/manifest.jsonmcp.jsonpackage.jsonscripts/build-mcpb.jsscripts/bump-version.cjsscripts/install-hooks.shscripts/pre-commit-secret-scan.shscripts/smoke-test.jssrc/__tests__/api-constraints.test.tssrc/__tests__/authentication.test.tssrc/__tests__/cache.test.tssrc/__tests__/config.test.tssrc/__tests__/connection-pool.test.tssrc/__tests__/docs-tools.test.tssrc/__tests__/helpscout-client.test.tssrc/__tests__/html-sanitize.test.tssrc/__tests__/html-to-markdown-live.test.tssrc/__tests__/html-to-markdown.test.tssrc/__tests__/index.test.tssrc/__tests__/integration.test.tssrc/__tests__/logger.test.tssrc/__tests__/mcpb-validation.test.tssrc/__tests__/mime.test.tssrc/__tests__/prompts.test.tssrc/__tests__/resources.test.tssrc/__tests__/schema.test.tssrc/__tests__/tools.test.tssrc/index.tssrc/prompts/index.tssrc/resources/docs-resources.tssrc/resources/index.tssrc/schema/types.tssrc/tools/docs-tools.tssrc/tools/index.tssrc/tools/reports-tools.tssrc/tools/tool-utils.tssrc/utils/api-constraints.tssrc/utils/cache.tssrc/utils/collection-resolver.tssrc/utils/config.tssrc/utils/helpscout-client.tssrc/utils/helpscout-docs-client.tssrc/utils/html-sanitize.tssrc/utils/logger.tssrc/utils/mcp-errors.tssrc/utils/mime.tssrc/utils/reports-api-client.tssrc/utils/service-container.tssrc/utils/site-resolver.tstest-docker-ci.cjstest-docker.cjstest_search_examples.mdtsconfig.json
💤 Files with no reviewable changes (9)
- PRODUCTION_WORKFLOW.md
- Dockerfile
- claude-desktop-config.json
- .eslintrc.json
- .github/workflows/update-docker-readme.yml
- test_search_examples.md
- docker-compose.yml
- test-docker.cjs
- test-docker-ci.cjs
| grep -rE 'qCJgfjX8a34wTWgn7yWYHUBP6TGHcGFa|<old_app_id>' . --exclude-dir=node_modules --exclude-dir=.git | ||
|
|
||
| # If found in any file, delete and re-commit | ||
| # If found in git history, run filter-repo: | ||
| git filter-repo --replace-text <(echo '<old_app_id>==>REVOKED_APP_ID') | ||
| git push --force --tags origin main | ||
| ``` | ||
|
|
||
| The replacement-text approach is non-destructive (commits keep their structure, just the credential strings get redacted with `REVOKED_APP_ID`). Safer than `--invert-paths` if the file containing the secret had other useful changes. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify git-filter-repo syntax and availability
which git-filter-repo || echo "git-filter-repo not installed; check installation notes."
# Test the replace-text syntax with a dummy pattern
# (dry-run mode if available, or in a test repo)
git --version && echo "Git available; verify filter-repo installation separately."Repository: GravityKit/help-scout-mcp
Length of output: 2002
🌐 Web query:
git filter-repo --replace-text syntax process substitution vs file argument
💡 Result:
The git filter-repo --replace-text option is designed to accept a file path as its argument, which should contain a list of search-and-replace expressions [1][2]. File Argument The standard and most robust way to use this option is by creating a dedicated file (e.g., expressions.txt) containing your replacement rules [1][3]. Each line in this file follows the format: [pattern_type:]expression==>replacement If the ==> and replacement part are omitted, the tool defaults the replacement to REMOVED [1][4]. If no prefix is specified, the expression is treated as a literal string, though regex: and glob: prefixes are also supported [1][5][4]. Process Substitution Bash process substitution, using the syntax <(echo 'find==>replace'), is a convenient shorthand often used to pass a temporary file descriptor to --replace-text without creating a physical file on disk [6][7]. While commonly cited in examples, this approach relies on the shell's ability to provide a file-like path (often in /proc/self/fd/ on Linux) that the tool can open and read [8]. Comparison and Limitations * Reliability: Using a physical file argument is the most compatible and reliable method across different operating systems and shell environments [8]. * Compatibility: Process substitution may fail on non-Linux systems (such as some macOS environments or Windows) because the underlying shell mechanisms for creating these file descriptors may not be available or may behave inconsistently [8]. * Use Case: Process substitution is primarily intended for quick, single-line replacements where you want to avoid cluttering your workspace with temporary files [6]. For complex or multiple replacement rules, a standard file argument is strongly recommended for better maintainability and error handling [1][3].
Citations:
- 1: https://github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-repo.txt
- 2: https://raw.githubusercontent.com/newren/git-filter-repo/refs/heads/docs/man1/git-filter-repo.1
- 3: https://www.mintlify.com/newren/git-filter-repo/guides/content-based-filtering
- 4: https://www.mankier.com/1/git-filter-repo
- 5: https://stackoverflow.com/questions/46950829/how-to-replace-a-string-in-whole-git-history
- 6: https://peterbabic.com/blog/clever-uses-for-git-filter-repo/
- 7: https://forrestcli.com/context/git-filter-repo/git-filter-repo:tldr:5cf1e
- 8: replace-text does not accept temporary files from shell
<(echo 'FROM==>TO')argument: no such file or directory '/proc/XXXX/fd/XX' newren/git-filter-repo#467
🌐 Web query:
git-filter-repo installation requirements team machines prerequisites
💡 Result:
To install git-filter-repo, the primary prerequisites are Git (version 2.36.0 or later) and Python 3 (version 3.6 or later) [1][2]. The tool is designed to be easily deployed because it is essentially a single-file Python script [3][1]. For basic use, you do not need a complex installation process; you can simply download the git-filter-repo script from the official repository and place it anywhere in your system's $PATH [4][1]. Advanced installation or specific environments may require additional steps: 1. Package Managers: On macOS, you can install it via Homebrew using brew install git-filter-repo [5]. On many Linux distributions, it may be available via your system package manager or through pip3 install git-filter-repo [6]. 2. Python Executables: If your system uses a python3 executable named something other than python3 (e.g., just python), you may need to edit the first line of the script to point to the correct interpreter [4][1]. 3. Documentation: To use the --help command with manpage or HTML support, the respective files must be placed in the directories specified by git --man-path or git --html-path [4]. 4. Library Usage: If you intend to use git-filter-repo as a Python library for custom filtering scripts, ensure that your environment supports standard Python library inclusion and be aware that the author does not guarantee backward compatibility for internal APIs [7]. For most team machine setups, ensuring Git >= 2.36.0 and Python >= 3.6 are installed is sufficient to run the core functionality by placing the script in a directory already in the user's $PATH [1][2].
Citations:
- 1: https://github.com/newren/git-filter-repo/blob/main/README.md
- 2: https://github.com/newren/git-filter-repo?tab=readme-ov-file
- 3: https://pypi.org/project/git-filter-repo/
- 4: https://github.com/newren/git-filter-repo/blob/main/INSTALL.md
- 5: https://formulae.brew.sh/formula/git-filter-repo
- 6: https://askubuntu.com/questions/1423729/how-to-install-git-filter-repo
- 7: https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo
🌐 Web query:
git filter-repo best practices credential rotation runbook documentation
💡 Result:
When sensitive data like credentials, API keys, or passwords are committed to Git, they must be considered permanently compromised [1][2][3][4]. The following best practices and runbook outline the safe procedure for remediation using git-filter-repo. Crucial Warning: Always perform these operations on a fresh clone of the repository to avoid data loss or repository corruption [2][3]. 1. Immediate Credential Rotation (Critical Step) Before attempting any history manipulation, revoke and rotate the exposed credentials at their source (e.g., AWS, GitHub, database providers) [5][1][6][7]. Rewriting history does not invalidate a key already in the hands of an attacker; rotation is the only way to ensure the credential is no longer usable [3][4]. 2. History Rewrite using git-filter-repo Use git-filter-repo, the officially recommended tool for this task [2][3]. - Clone the repository as a mirror: git clone --mirror repo-clean.git cd repo-clean.git - Use the --sensitive-data-removal (--sdr) flag to assist in cleanup and generate instructions [5][6][8]. - Remove the specific file or pattern: git filter-repo --path <path/to/secret/file> --invert-paths --sensitive-data-removal - Alternatively, use a file containing patterns for text replacement: git filter-repo --replace-text <secrets.txt> --force - Inspect the repository locally to ensure the sensitive data is gone [6]. 3. Force-Push and Cleanup - Force-push the rewritten history to all refs [1][3][6]: git push --force --all origin git push --force --tags origin - Coordinate with your team: All contributors must delete their existing local clones and perform a fresh re-clone [1][2][3]. Instruct them NOT to use git pull, as it will merge the old, insecure history back into the repository [1]. 4. Server-Side Cleanup If the repository is hosted on platforms like GitHub, contact their support if you cannot remove cached views or specific objects (like pull requests) that continue to expose the data [7]. Provide them with the First Changed Commit(s) ID output by git-filter-repo [7]. 5. Prevention (Future-Proofing) - Integrate pre-commit hooks (e.g., gitleaks, git-secrets) to scan for sensitive patterns before they are committed [5][2][3]. - Enable server-side push protection provided by platforms like GitHub [3]. - Use environment variables or secret management services (e.g., AWS Secrets Manager, HashiCorp Vault) rather than hardcoding secrets in configuration files [5][2]. - Add files containing secrets to.gitignore to prevent accidental tracking [5][2]. Note: git-filter-repo is designed for one-shot history rewriting [9]. Do not attempt to use it to filter multiple clones of the same repository [9]. [5][1][2][3][7]
Citations:
- 1: https://www.mintlify.com/newren/git-filter-repo/use-cases/removing-sensitive-data
- 2: https://techcommunity.microsoft.com/blog/azureinfrastructureblog/how-to-safely-remove-secrets-from-your-git-history-the-right-way/4464722
- 3: https://how2.sh/posts/how-to-clean-up-sensitive-data-from-git-history-with-git-filter-repo/
- 4: https://techearl.com/git-remove-secret-from-history
- 5: https://github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-repo.txt
- 6: https://raw.githubusercontent.com/newren/git-filter-repo/refs/heads/docs/man1/git-filter-repo.1
- 7: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository
- 8: https://manpages.debian.org/unstable/git-filter-repo/git-filter-repo.1.en.html
- 9: https://github.com/newren/git-filter-repo/blob/main/Documentation/FAQ.md
Add installation instructions and clarify placeholder secret; use file argument instead of process substitution for cross-platform compatibility.
Line 88 uses a hardcoded grep pattern example that operators must replace with their actual leaked secret. Lines 91-93 invoke git filter-repo --replace-text with bash process substitution, which works on Linux but is not reliably cross-platform—it may fail on macOS and Windows systems where /proc/self/fd is unavailable.
Specific improvements needed:
-
Installation steps: Add explicit instructions to install
git-filter-repo(requires Git ≥2.36.0 and Python ≥3.6; available viabrew install git-filter-repo,pip3 install git-filter-repo, or manual script placement in$PATH). -
Explicit placeholder substitution: Change the comment or code example to clearly state "Replace
qCJgfjX8a34wTWgn7yWYHUBP6TGHcGFawith the actual old App Secret you revoked." -
Use file argument instead of process substitution: Replace the process substitution syntax with a stable file-based approach:
echo '<old_app_id>==>REVOKED_APP_ID' > /tmp/replacements.txt git filter-repo --replace-text /tmp/replacements.txt rm /tmp/replacements.txt
This avoids cross-platform compatibility issues and is the officially recommended pattern.
Operators unfamiliar with these tools could fail to rotate credentials properly or accidentally corrupt history if these steps are unclear.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/runbooks/credential-rotation.md` around lines 88 - 96, The credential
rotation runbook uses bash process substitution in the git filter-repo command
which is not cross-platform compatible, lacks installation instructions for
git-filter-repo, and does not clearly indicate that the hardcoded grep pattern
example must be replaced with an actual leaked secret. Add a prerequisites
section before the grep command with installation instructions for
git-filter-repo (including minimum version requirements and installation
methods). Replace the hardcoded pattern qCJgfjX8a34wTWgn7yWYHUBP6TGHcGFa in the
grep example with a clear placeholder comment stating operators must substitute
their actual revoked secret. Replace the process substitution syntax in the git
filter-repo --replace-text command with a file-based approach by creating a
temporary replacement file, passing it as an argument to git filter-repo, and
then cleaning up the temporary file to ensure the runbook works reliably across
Linux, macOS, and Windows systems.
| "runtimes": { | ||
| "node": ">=18.0.0" | ||
| } |
There was a problem hiding this comment.
Node runtime constraint is inconsistent with package.json.
The manifest declares "node": ">=18.0.0" but package.json requires "node": ">=20.0.0". The CI workflow also dropped Node 18 because the code uses globalThis.File which only became global in Node 20.
Users installing via this manifest on Node 18.x will encounter runtime failures.
🐛 Fix: Align Node constraint with package.json
"runtimes": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "runtimes": { | |
| "node": ">=18.0.0" | |
| } | |
| "runtimes": { | |
| "node": ">=20.0.0" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@helpscout-mcp-extension/manifest.json` around lines 27 - 29, The Node runtime
constraint in the manifest.json file under the runtimes section is set to
">=18.0.0" but this is inconsistent with the package.json requirement of
">=20.0.0" and the codebase uses globalThis.File which is only available in Node
20 and above. Update the "node" value in the "runtimes" object in manifest.json
from ">=18.0.0" to ">=20.0.0" to match the package.json constraint and ensure
consistency across all configuration files.
| git clone https://github.com/jgalea/help-scout-mcp.git | ||
| cd help-scout-mcp |
There was a problem hiding this comment.
Use one canonical repository URL in setup commands.
Line 23-24 and Line 219-220 currently clone jgalea/help-scout-mcp, which conflicts with the repo this PR targets (GravityKit/help-scout-mcp) and with CONTRIBUTING.md. This can send users to the wrong codebase.
🔧 Suggested doc fix
-git clone https://github.com/jgalea/help-scout-mcp.git
+git clone https://github.com/GravityKit/help-scout-mcp.git
cd help-scout-mcpAlso applies to: 219-220
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 23 - 24, Update all git clone commands in README.md
that reference the `jgalea/help-scout-mcp` repository to use the canonical
repository URL `GravityKit/help-scout-mcp` instead. This includes the clone
command in the setup instructions section and the one that also appears later in
the document. Ensure consistency across the README.md with the repository
targeted by this PR and with the CONTRIBUTING.md documentation.
| // Stage the changes | ||
| execSync('git add package.json Dockerfile src/index.ts mcp.json helpscout-mcp-extension/manifest.json'); | ||
| execSync('git add package.json src/index.ts mcp.json helpscout-mcp-extension/manifest.json'); |
There was a problem hiding this comment.
Version bump updates Dockerfile but does not commit it.
updateDockerfile(newVersion) runs in Line 136, but Line 94 no longer stages Dockerfile. That leaves versioned release commits/tags with partial updates.
🔧 Suggested fix
- execSync('git add package.json src/index.ts mcp.json helpscout-mcp-extension/manifest.json');
+ execSync('git add package.json src/index.ts mcp.json helpscout-mcp-extension/manifest.json Dockerfile');Also applies to: 136-139
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/bump-version.cjs` at line 94, The git add command in the execSync
call is missing Dockerfile from the list of files to stage, even though
updateDockerfile(newVersion) modifies it on line 136. Add Dockerfile to the git
add command alongside the other version-related files (package.json,
src/index.ts, mcp.json, helpscout-mcp-extension/manifest.json) to ensure the
Dockerfile changes are included in the version bump commit.
| hits=$(git diff --cached -U0 -- ':(exclude)*.test.ts' ':(exclude)*.test.js' ':(exclude)__tests__/*' \ | ||
| | grep -E "^\+" \ | ||
| | grep -iE "$pattern" \ | ||
| | grep -v 'your-app-secret\|your-app-id\|YOUR_APP_SECRET\|YOUR_APP_ID\|example\|placeholder\|REDACTED' \ | ||
| || true) |
There was a problem hiding this comment.
Secret scanning currently skips test files, leaving an easy leak path.
On Line 47, test paths are excluded from scan input, so secrets added in tests bypass this hook entirely.
Suggested fix
-for pattern in "${patterns[@]}"; do
- hits=$(git diff --cached -U0 -- ':(exclude)*.test.ts' ':(exclude)*.test.js' ':(exclude)__tests__/*' \
+for pattern in "${patterns[@]}"; do
+ hits=$(git diff --cached -U0 -- \
| grep -E "^\+" \
| grep -iE "$pattern" \
| grep -v 'your-app-secret\|your-app-id\|YOUR_APP_SECRET\|YOUR_APP_ID\|example\|placeholder\|REDACTED' \
|| true)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/pre-commit-secret-scan.sh` around lines 47 - 51, Remove the exclusion
patterns for test files in the git diff command within the hits variable
assignment. Currently, the patterns ':(exclude)*.test.ts',
':(exclude)*.test.js', and ':(exclude)__tests__/*' are preventing test files
from being scanned for secrets. Delete these exclusion patterns from the git
diff command so that all files, including test files, are included in the secret
scanning check.
| ## Tool Selection Guide | ||
| | Task | Tool | | ||
| |------|------| | ||
| | Find tickets by keyword (billing, refund, bug) | searchConversations with searchTerms | | ||
| | List recent/filtered tickets | searchConversations with status/date/inbox | | ||
| | Complex filters (email domain, multiple tags) | searchConversations with contentTerms/customerEmail/tags | | ||
| | Summarize recent tickets with transcripts | searchConversations with includeTranscripts:true | | ||
| | Lookup by ticket number (#12345) | structuredConversationFilter | | ||
| | Get full conversation thread | getThreads | | ||
| | Quick conversation preview | getConversationSummary |${docsRow} | ||
| | Get report data | getReport, getHappinessRatings | | ||
|
|
There was a problem hiding this comment.
Remove deprecated report tool naming from instructions.
The tool guide still advertises getHappinessRatings. If reports are routed through merged getReport, this can drive invalid tool calls from clients.
As per coding guidelines, src/tools/reports-tools.ts: “Reports use merged entrypoint getReport.”
Suggested patch
-| Get report data | getReport, getHappinessRatings |
+| Get report data | getReport |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## Tool Selection Guide | |
| | Task | Tool | | |
| |------|------| | |
| | Find tickets by keyword (billing, refund, bug) | searchConversations with searchTerms | | |
| | List recent/filtered tickets | searchConversations with status/date/inbox | | |
| | Complex filters (email domain, multiple tags) | searchConversations with contentTerms/customerEmail/tags | | |
| | Summarize recent tickets with transcripts | searchConversations with includeTranscripts:true | | |
| | Lookup by ticket number (#12345) | structuredConversationFilter | | |
| | Get full conversation thread | getThreads | | |
| | Quick conversation preview | getConversationSummary |${docsRow} | |
| | Get report data | getReport, getHappinessRatings | | |
| ## Tool Selection Guide | |
| | Task | Tool | | |
| |------|------| | |
| | Find tickets by keyword (billing, refund, bug) | searchConversations with searchTerms | | |
| | List recent/filtered tickets | searchConversations with status/date/inbox | | |
| | Complex filters (email domain, multiple tags) | searchConversations with contentTerms/customerEmail/tags | | |
| | Summarize recent tickets with transcripts | searchConversations with includeTranscripts:true | | |
| | Lookup by ticket number (`#12345`) | structuredConversationFilter | | |
| | Get full conversation thread | getThreads | | |
| | Quick conversation preview | getConversationSummary |${docsRow} | |
| | Get report data | getReport | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/index.ts` around lines 91 - 102, The Tool Selection Guide in src/index.ts
still references the deprecated getHappinessRatings tool in the "Get report
data" row, which has been merged into the getReport endpoint. Remove the mention
of getHappinessRatings from the guide and update the "Get report data" row to
only reference getReport, ensuring the documentation is consistent with the
actual merged tool implementation defined in src/tools/reports-tools.ts and
prevents clients from making invalid tool calls.
Source: Coding guidelines
| export const StructuredConversationFilterInputSchema = z.object({ | ||
| assignedTo: z.number().int().min(-1).describe('User ID (-1 for unassigned)').optional(), | ||
| folderId: z.number().int().min(1).describe('Folder ID must be positive').optional(), | ||
| customerIds: z.array(z.number().int().min(0)).max(100).describe('Max 100 customer IDs').optional(), | ||
| conversationNumber: z.number().int().min(1).describe('Conversation number must be positive').optional(), | ||
| status: z.enum(['active', 'pending', 'closed', 'spam', 'all']).default('all'), | ||
| inboxId: z.string().optional(), | ||
| statuses: z.array(z.enum(['active', 'pending', 'closed', 'spam'])).default(['active', 'pending', 'closed']), | ||
| searchIn: z.array(z.enum(['body', 'subject', 'both'])).default(['both']), | ||
| timeframeDays: z.number().min(1).max(365).default(60), | ||
| tag: z.string().optional(), | ||
| createdAfter: z.string().optional(), | ||
| createdBefore: z.string().optional(), | ||
| limitPerStatus: z.number().min(1).max(100).default(25), | ||
| includeVariations: z.boolean().default(true), | ||
| }); | ||
| modifiedSince: z.string().optional(), | ||
| sortBy: z.enum(['createdAt', 'modifiedAt', 'number', 'waitingSince', 'customerName', 'customerEmail', 'mailboxId', 'status', 'subject']).default('createdAt'), | ||
| sortOrder: z.enum(['asc', 'desc']).default('desc'), | ||
| limit: z.number().min(1).max(200).default(50), | ||
| cursor: z.string().optional(), | ||
| }).refine( | ||
| (data) => !!(data.assignedTo !== undefined || data.folderId !== undefined || data.customerIds !== undefined || data.conversationNumber !== undefined || (data.sortBy && ['waitingSince', 'customerName', 'customerEmail'].includes(data.sortBy))), | ||
| { message: 'Must use at least one unique field: assignedTo, folderId, customerIds, conversationNumber, or unique sorting. For content search, use searchConversations with searchTerms.' } |
There was a problem hiding this comment.
Refine allows empty customerIds, bypassing the intended “unique field” requirement.
Line 152 checks data.customerIds !== undefined, so customerIds: [] passes validation. Downstream (src/tools/index.ts structured filter path) only applies customerIds when length > 0, so this becomes an unintentionally broad query.
Suggested fix
export const StructuredConversationFilterInputSchema = z.object({
assignedTo: z.number().int().min(-1).describe('User ID (-1 for unassigned)').optional(),
folderId: z.number().int().min(1).describe('Folder ID must be positive').optional(),
- customerIds: z.array(z.number().int().min(0)).max(100).describe('Max 100 customer IDs').optional(),
+ customerIds: z.array(z.number().int().min(0)).min(1).max(100).describe('1-100 customer IDs').optional(),
conversationNumber: z.number().int().min(1).describe('Conversation number must be positive').optional(),
@@
}).refine(
- (data) => !!(data.assignedTo !== undefined || data.folderId !== undefined || data.customerIds !== undefined || data.conversationNumber !== undefined || (data.sortBy && ['waitingSince', 'customerName', 'customerEmail'].includes(data.sortBy))),
+ (data) => !!(
+ data.assignedTo !== undefined ||
+ data.folderId !== undefined ||
+ (data.customerIds?.length ?? 0) > 0 ||
+ data.conversationNumber !== undefined ||
+ (data.sortBy && ['waitingSince', 'customerName', 'customerEmail'].includes(data.sortBy))
+ ),
{ message: 'Must use at least one unique field: assignedTo, folderId, customerIds, conversationNumber, or unique sorting. For content search, use searchConversations with searchTerms.' }
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const StructuredConversationFilterInputSchema = z.object({ | |
| assignedTo: z.number().int().min(-1).describe('User ID (-1 for unassigned)').optional(), | |
| folderId: z.number().int().min(1).describe('Folder ID must be positive').optional(), | |
| customerIds: z.array(z.number().int().min(0)).max(100).describe('Max 100 customer IDs').optional(), | |
| conversationNumber: z.number().int().min(1).describe('Conversation number must be positive').optional(), | |
| status: z.enum(['active', 'pending', 'closed', 'spam', 'all']).default('all'), | |
| inboxId: z.string().optional(), | |
| statuses: z.array(z.enum(['active', 'pending', 'closed', 'spam'])).default(['active', 'pending', 'closed']), | |
| searchIn: z.array(z.enum(['body', 'subject', 'both'])).default(['both']), | |
| timeframeDays: z.number().min(1).max(365).default(60), | |
| tag: z.string().optional(), | |
| createdAfter: z.string().optional(), | |
| createdBefore: z.string().optional(), | |
| limitPerStatus: z.number().min(1).max(100).default(25), | |
| includeVariations: z.boolean().default(true), | |
| }); | |
| modifiedSince: z.string().optional(), | |
| sortBy: z.enum(['createdAt', 'modifiedAt', 'number', 'waitingSince', 'customerName', 'customerEmail', 'mailboxId', 'status', 'subject']).default('createdAt'), | |
| sortOrder: z.enum(['asc', 'desc']).default('desc'), | |
| limit: z.number().min(1).max(200).default(50), | |
| cursor: z.string().optional(), | |
| }).refine( | |
| (data) => !!(data.assignedTo !== undefined || data.folderId !== undefined || data.customerIds !== undefined || data.conversationNumber !== undefined || (data.sortBy && ['waitingSince', 'customerName', 'customerEmail'].includes(data.sortBy))), | |
| { message: 'Must use at least one unique field: assignedTo, folderId, customerIds, conversationNumber, or unique sorting. For content search, use searchConversations with searchTerms.' } | |
| export const StructuredConversationFilterInputSchema = z.object({ | |
| assignedTo: z.number().int().min(-1).describe('User ID (-1 for unassigned)').optional(), | |
| folderId: z.number().int().min(1).describe('Folder ID must be positive').optional(), | |
| customerIds: z.array(z.number().int().min(0)).min(1).max(100).describe('1-100 customer IDs').optional(), | |
| conversationNumber: z.number().int().min(1).describe('Conversation number must be positive').optional(), | |
| status: z.enum(['active', 'pending', 'closed', 'spam', 'all']).default('all'), | |
| inboxId: z.string().optional(), | |
| tag: z.string().optional(), | |
| createdAfter: z.string().optional(), | |
| createdBefore: z.string().optional(), | |
| modifiedSince: z.string().optional(), | |
| sortBy: z.enum(['createdAt', 'modifiedAt', 'number', 'waitingSince', 'customerName', 'customerEmail', 'mailboxId', 'status', 'subject']).default('createdAt'), | |
| sortOrder: z.enum(['asc', 'desc']).default('desc'), | |
| limit: z.number().min(1).max(200).default(50), | |
| cursor: z.string().optional(), | |
| }).refine( | |
| (data) => !!( | |
| data.assignedTo !== undefined || | |
| data.folderId !== undefined || | |
| (data.customerIds?.length ?? 0) > 0 || | |
| data.conversationNumber !== undefined || | |
| (data.sortBy && ['waitingSince', 'customerName', 'customerEmail'].includes(data.sortBy)) | |
| ), | |
| { message: 'Must use at least one unique field: assignedTo, folderId, customerIds, conversationNumber, or unique sorting. For content search, use searchConversations with searchTerms.' } | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/schema/types.ts` around lines 136 - 153, In the
StructuredConversationFilterInputSchema refine validation logic, the check for
customerIds uses only data.customerIds !== undefined, which allows empty arrays
to pass the "unique field" requirement. Update the condition to verify that
customerIds is not only defined but also has a non-zero length (e.g.,
data.customerIds && data.customerIds.length > 0) so that empty arrays do not
satisfy the validation requirement and trigger the appropriate error message.
| set<T>(prefix: string, data: unknown, value: T, options?: CacheOptions): void { | ||
| const key = this.generateKey(prefix, data); | ||
| const ttl = options?.ttl ? options.ttl * 1000 : this.defaultTtl; | ||
|
|
There was a problem hiding this comment.
ttl: 0 is currently ignored and replaced with default TTL.
On Line 79, options?.ttl ? ... : ... treats 0 as falsy, but callers already pass ttl: 0 intentionally. That causes unexpected caching instead of immediate expiry/no-cache behavior.
Suggested fix
- const ttl = options?.ttl ? options.ttl * 1000 : this.defaultTtl;
+ const ttl = options?.ttl !== undefined ? options.ttl * 1000 : this.defaultTtl;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/cache.ts` around lines 77 - 80, The set method in the cache utility
is using a falsy check on options?.ttl, which incorrectly treats the explicit
value ttl: 0 as undefined and falls back to the default TTL. Fix the TTL
assignment logic on line 79 by replacing the truthiness check (options?.ttl ?)
with a proper nullish coalescing check (options?.ttl !== undefined) so that
callers who intentionally pass ttl: 0 to disable caching have that value
respected instead of being overridden with the default TTL.
| /** | ||
| * Clear idle connections to free up resources | ||
| */ | ||
| clearIdleConnections(): void { | ||
| const stats = this.getPoolStats(); | ||
|
|
||
| // Force destroy all agent connections by recreating them | ||
| this.httpAgent.destroy(); | ||
| this.httpsAgent.destroy(); | ||
|
|
||
| // Recreate agents with same configuration | ||
| const poolConfig = { | ||
| keepAlive: true, | ||
| keepAliveMsecs: 1000, | ||
| maxSockets: 20, | ||
| maxFreeSockets: 5, | ||
| timeout: 30000, | ||
| }; | ||
|
|
||
| this.httpAgent = new HttpAgent(poolConfig); | ||
| this.httpsAgent = new HttpsAgent(poolConfig); | ||
|
|
||
| logger.debug('Cleared Docs idle connections', { | ||
| clearedHttp: stats.http.freeSockets, | ||
| clearedHttps: stats.https.freeSockets, | ||
| }); | ||
| } |
There was a problem hiding this comment.
clearIdleConnections() orphans the Axios instance from newly created agents.
After destroying and recreating this.httpAgent / this.httpsAgent, the this.client Axios instance still holds references to the old (destroyed) agents configured during construction. Subsequent requests will fail or behave unexpectedly.
Either update the Axios instance's defaults, or recreate the instance:
clearIdleConnections(): void {
const stats = this.getPoolStats();
this.httpAgent.destroy();
this.httpsAgent.destroy();
const poolConfig = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 20,
maxFreeSockets: 5,
timeout: 30000,
};
this.httpAgent = new HttpAgent(poolConfig);
this.httpsAgent = new HttpsAgent(poolConfig);
+
+ // Update the Axios instance to use the new agents
+ this.client.defaults.httpAgent = this.httpAgent;
+ this.client.defaults.httpsAgent = this.httpsAgent;
logger.debug('Cleared Docs idle connections', {
clearedHttp: stats.http.freeSockets,
clearedHttps: stats.https.freeSockets,
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Clear idle connections to free up resources | |
| */ | |
| clearIdleConnections(): void { | |
| const stats = this.getPoolStats(); | |
| // Force destroy all agent connections by recreating them | |
| this.httpAgent.destroy(); | |
| this.httpsAgent.destroy(); | |
| // Recreate agents with same configuration | |
| const poolConfig = { | |
| keepAlive: true, | |
| keepAliveMsecs: 1000, | |
| maxSockets: 20, | |
| maxFreeSockets: 5, | |
| timeout: 30000, | |
| }; | |
| this.httpAgent = new HttpAgent(poolConfig); | |
| this.httpsAgent = new HttpsAgent(poolConfig); | |
| logger.debug('Cleared Docs idle connections', { | |
| clearedHttp: stats.http.freeSockets, | |
| clearedHttps: stats.https.freeSockets, | |
| }); | |
| } | |
| /** | |
| * Clear idle connections to free up resources | |
| */ | |
| clearIdleConnections(): void { | |
| const stats = this.getPoolStats(); | |
| // Force destroy all agent connections by recreating them | |
| this.httpAgent.destroy(); | |
| this.httpsAgent.destroy(); | |
| // Recreate agents with same configuration | |
| const poolConfig = { | |
| keepAlive: true, | |
| keepAliveMsecs: 1000, | |
| maxSockets: 20, | |
| maxFreeSockets: 5, | |
| timeout: 30000, | |
| }; | |
| this.httpAgent = new HttpAgent(poolConfig); | |
| this.httpsAgent = new HttpsAgent(poolConfig); | |
| // Update the Axios instance to use the new agents | |
| this.client.defaults.httpAgent = this.httpAgent; | |
| this.client.defaults.httpsAgent = this.httpsAgent; | |
| logger.debug('Cleared Docs idle connections', { | |
| clearedHttp: stats.http.freeSockets, | |
| clearedHttps: stats.https.freeSockets, | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/helpscout-docs-client.ts` around lines 590 - 616, The
clearIdleConnections() method destroys and recreates this.httpAgent and
this.httpsAgent, but the this.client Axios instance still holds references to
the old destroyed agents from when it was originally constructed. This causes
subsequent requests to fail because the Axios instance continues using the
destroyed agents. After recreating the agents with the poolConfig, you must also
update the Axios instance to use these new agents by either updating
this.client's httpAgent and httpsAgent defaults or by recreating the this.client
instance entirely with the new agents.
| /** | ||
| * Base class with dependency injection support | ||
| */ | ||
| export abstract class Injectable { | ||
| protected services: ServiceResolver; | ||
|
|
||
| constructor(container?: ServiceContainer) { | ||
| this.services = (container || ServiceContainer.getInstance()).createResolver(); | ||
| } | ||
|
|
||
| /** | ||
| * Get a specific service | ||
| */ | ||
| protected getService<K extends keyof ServiceRegistry>(key: K): ServiceRegistry[K] { | ||
| return ServiceContainer.getInstance().get(key); | ||
| } | ||
| } |
There was a problem hiding this comment.
getService ignores the injected container, breaking test isolation.
The Injectable constructor accepts a custom container and stores a resolver from it, but getService always calls ServiceContainer.getInstance(), bypassing the injected container. Tests using createTestContainer() won't get their mocked services.
protected getService<K extends keyof ServiceRegistry>(key: K): ServiceRegistry[K] {
- return ServiceContainer.getInstance().get(key);
+ return this.services.resolve([key])[key];
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Base class with dependency injection support | |
| */ | |
| export abstract class Injectable { | |
| protected services: ServiceResolver; | |
| constructor(container?: ServiceContainer) { | |
| this.services = (container || ServiceContainer.getInstance()).createResolver(); | |
| } | |
| /** | |
| * Get a specific service | |
| */ | |
| protected getService<K extends keyof ServiceRegistry>(key: K): ServiceRegistry[K] { | |
| return ServiceContainer.getInstance().get(key); | |
| } | |
| } | |
| /** | |
| * Base class with dependency injection support | |
| */ | |
| export abstract class Injectable { | |
| protected services: ServiceResolver; | |
| constructor(container?: ServiceContainer) { | |
| this.services = (container || ServiceContainer.getInstance()).createResolver(); | |
| } | |
| /** | |
| * Get a specific service | |
| */ | |
| protected getService<K extends keyof ServiceRegistry>(key: K): ServiceRegistry[K] { | |
| return this.services.resolve([key])[key]; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/service-container.ts` around lines 220 - 236, The getService method
in the Injectable class is ignoring the injected container and always calling
ServiceContainer.getInstance() directly, which bypasses the custom resolver
stored in this.services. Fix this by changing the getService method to use the
injected resolver this.services.get(key) instead of
ServiceContainer.getInstance().get(key), so that tests using a custom container
will receive their mocked services instead of the global singleton.
Summary by CodeRabbit
New Features
Security & Hardening
Configuration
HELPSCOUT_APP_ID/HELPSCOUT_APP_SECRET)Documentation