Skip to content

Jgalea patch 1 - #4

Open
jgalea wants to merge 109 commits into
GravityKit:mainfrom
jgalea:jgalea-patch-1
Open

Jgalea patch 1#4
jgalea wants to merge 109 commits into
GravityKit:mainfrom
jgalea:jgalea-patch-1

Conversation

@jgalea

@jgalea jgalea commented Jun 18, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • OAuth2-based credential system with macOS Keychain support
    • HTML sanitization for reply and documentation content
    • Reports and Docs API tool integration with resource handlers
    • PII redaction and log sanitization
    • Pre-commit secret scanning for credential protection
  • Security & Hardening

    • Default-deny authorization for write tools with explicit allowlists
    • Secure attachment handling with restricted file permissions
    • Enhanced API error redaction and request ID generation
    • Host allowlisting for API endpoints and docs sites
  • Configuration

    • Environment variables updated to OAuth2 pattern (HELPSCOUT_APP_ID/HELPSCOUT_APP_SECRET)
    • New control flags: write inbox/docs allowlists, content redaction, reply spacing
    • Keychain integration for credential storage (macOS)
  • Documentation

    • Security policy and hardening guidance added
    • Credential rotation runbook provided
    • Updated API reference and configuration documentation

zackkatz and others added 30 commits October 13, 2025 23:27
- 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>
jgalea added 28 commits April 25, 2026 12:08
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
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This 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.

Changes

Core fork refresh

Layer / File(s) Summary
Repository, packaging, and published surface
.env.example, .github/*, .gitignore, .semgrepignore, package.json, mcp.json, helpscout-mcp-extension/*, README.md, SECURITY.md, AGENTS.md, CONTRIBUTING.md, LICENSE, tsconfig.json
Updates repo automation, package and manifest metadata, MCPB packaging, example env vars, ignore rules, and top-level fork documentation to match the new OAuth2 and tool surface contracts.
Runtime config, auth, logging, and cache
src/utils/config.ts, src/utils/helpscout-client.ts, src/utils/logger.ts, src/utils/cache.ts, src/utils/html-sanitize.ts, src/utils/mime.ts, src/utils/mcp-errors.ts
Adds Keychain-backed OAuth2 config loading, stricter validation and allowlists, sanitized logging and API errors, prefix-based cache clearing, HTML sanitizers, MIME helpers, and updated mailbox client auth behavior.
Docs, reports, and service infrastructure
src/utils/helpscout-docs-client.ts, src/utils/reports-api-client.ts, src/utils/service-container.ts, src/utils/site-resolver.ts, src/utils/collection-resolver.ts, src/resources/*, src/tools/reports-tools.ts
Introduces Docs and Reports clients, service resolution utilities, docs resource handling, docs site or collection resolution, and report tool implementations.
Schemas, tools, prompts, and server bootstrap
src/schema/types.ts, src/utils/api-constraints.ts, src/tools/tool-utils.ts, src/prompts/index.ts, src/index.ts
Expands search, docs, reply, note, and conversation schemas, revises tool validation and prompt text, compacts tool metadata, and changes server startup to async inbox discovery with redacted request logging.
Behavioral validation and compatibility tests
src/__tests__/*, scripts/smoke-test.js
Rewrites and expands tests for config, auth, logging, resources, prompts, docs flows, transcripts, create/update tools, packaging validation, and runtime smoke checks.
Reference docs and operational runbooks
docs/help-scout-api-reference.md, help-scout-complete-api-documentation.md, docs/runbooks/credential-rotation.md
Adds large Help Scout API reference documents and a credential-rotation runbook for the fork’s OAuth2 operating flow.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • GravityKit/help-scout-mcp#3: Continues the Docs and Reports tool consolidation by touching shared tool routing, descriptions, and related tests.
  • GravityKit/help-scout-mcp#2: Overlaps the thread and transcript behavior changes around getThreads, draft exclusion, and transcript handling.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Jgalea patch 1' is vague and generic, providing minimal information about the actual changes in the pull request. Revise the title to be more descriptive and specific. For example: 'Security hardening and tooling migration: OAuth2 auth, HTML sanitization, MCPB packaging' or similar, reflecting the major substantive changes across configuration, documentation, tests, and source code.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch jgalea-patch-1

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Line 109: Keychain cache guidance is misleading.

The suggestion that security delete-generic-password followed 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 the security delete-generic-password option 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 standard closePool() method; this is an implementation-specific pattern used by some SDKs. Confirm that src/index.ts or 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 win

Include statuses in status-presence detection.

Line 85 currently checks only status. If a caller provides statuses, 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 win

Always 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 win

Cache TTL comment says "24 hours" but value is 24 minutes.

The comment says // 24 hours for collections but 1440 seconds = 24 minutes, not 24 hours. If 24 hours is intended, use 86400.

 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 win

Empty 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 updating lastFetch, 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 win

Empty 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 win

Error 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 win

Cache hit/miss logs are wrong for falsy cached values.

On Line 68, if (value) marks valid cached values like 0, 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 win

Handle 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 to application/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 win

Docs 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 to UpdateConversationInputSchema.

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 win

Align the integration config mock with current runtime contracts.

Line 9 falling back clientId to HELPSCOUT_API_KEY, and Lines 34–36 honoring args.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: keep verbose support as environment-driven behavior; do not add a verbose tool 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 win

Consider 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. Adding persist-credentials: false to 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: false

Note: 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 win

Add tests for the newly introduced validator branches.

This file updates the searchTerms hint assertion, but the new validateToolCall branches for createReply, getConversation, createConversation, and updateConversation are 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 win

Assert redactArgs is 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

📥 Commits

Reviewing files that changed from the base of the PR and between e3fadb7 and 9c49c42.

⛔ Files ignored due to path filters (3)
  • helpscout-mcp-extension/icon.png is excluded by !**/*.png
  • helpscout-mcp-extension/icon.svg is excluded by !**/*.svg
  • package-lock.json is 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
  • .semgrepignore
  • AGENTS.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • Dockerfile
  • LICENSE
  • PRODUCTION_WORKFLOW.md
  • README.md
  • SECURITY.md
  • claude-desktop-config.json
  • docker-compose.yml
  • docs/help-scout-api-reference.md
  • docs/runbooks/credential-rotation.md
  • eslint.config.js
  • help-scout-complete-api-documentation.md
  • helpscout-mcp-extension/.gitignore
  • helpscout-mcp-extension/README.md
  • helpscout-mcp-extension/manifest.json
  • mcp.json
  • package.json
  • scripts/build-mcpb.js
  • scripts/bump-version.cjs
  • scripts/install-hooks.sh
  • scripts/pre-commit-secret-scan.sh
  • scripts/smoke-test.js
  • src/__tests__/api-constraints.test.ts
  • src/__tests__/authentication.test.ts
  • src/__tests__/cache.test.ts
  • src/__tests__/config.test.ts
  • src/__tests__/connection-pool.test.ts
  • src/__tests__/docs-tools.test.ts
  • src/__tests__/helpscout-client.test.ts
  • src/__tests__/html-sanitize.test.ts
  • src/__tests__/html-to-markdown-live.test.ts
  • src/__tests__/html-to-markdown.test.ts
  • src/__tests__/index.test.ts
  • src/__tests__/integration.test.ts
  • src/__tests__/logger.test.ts
  • src/__tests__/mcpb-validation.test.ts
  • src/__tests__/mime.test.ts
  • src/__tests__/prompts.test.ts
  • src/__tests__/resources.test.ts
  • src/__tests__/schema.test.ts
  • src/__tests__/tools.test.ts
  • src/index.ts
  • src/prompts/index.ts
  • src/resources/docs-resources.ts
  • src/resources/index.ts
  • src/schema/types.ts
  • src/tools/docs-tools.ts
  • src/tools/index.ts
  • src/tools/reports-tools.ts
  • src/tools/tool-utils.ts
  • src/utils/api-constraints.ts
  • src/utils/cache.ts
  • src/utils/collection-resolver.ts
  • src/utils/config.ts
  • src/utils/helpscout-client.ts
  • src/utils/helpscout-docs-client.ts
  • src/utils/html-sanitize.ts
  • src/utils/logger.ts
  • src/utils/mcp-errors.ts
  • src/utils/mime.ts
  • src/utils/reports-api-client.ts
  • src/utils/service-container.ts
  • src/utils/site-resolver.ts
  • test-docker-ci.cjs
  • test-docker.cjs
  • test_search_examples.md
  • tsconfig.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

Comment on lines +88 to +96
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🌐 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:


🌐 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:


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:

  1. Installation steps: Add explicit instructions to install git-filter-repo (requires Git ≥2.36.0 and Python ≥3.6; available via brew install git-filter-repo, pip3 install git-filter-repo, or manual script placement in $PATH).

  2. Explicit placeholder substitution: Change the comment or code example to clearly state "Replace qCJgfjX8a34wTWgn7yWYHUBP6TGHcGFa with the actual old App Secret you revoked."

  3. 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.

Comment on lines +27 to +29
"runtimes": {
"node": ">=18.0.0"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
"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.

Comment thread README.md
Comment on lines +23 to +24
git clone https://github.com/jgalea/help-scout-mcp.git
cd help-scout-mcp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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-mcp

Also 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.

Comment thread scripts/bump-version.cjs
// 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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +47 to +51
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread src/index.ts
Comment on lines +91 to +102
## 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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
## 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

Comment thread src/schema/types.ts
Comment on lines +136 to +153
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.' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment thread src/utils/cache.ts
Comment on lines 77 to +80
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +590 to +616
/**
* 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,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
/**
* 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.

Comment on lines +220 to +236
/**
* 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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
/**
* 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants