Skip to content

fix(mcp): harden send_feedback against integer-extra drift, error-log echo, and dispatcher shadowing - #4915

Merged
gesh merged 4 commits into
mainfrom
posthog/mcp-feedback-hardening
Sep 14, 2026
Merged

gesh merged 4 commits into
mainfrom
posthog/mcp-feedback-hardening

Conversation

@gesh

@gesh gesh commented Sep 11, 2026

Copy link
Copy Markdown
Member

Problem

While porting the collectFeedback feature (#4870) to posthog-python (PostHog/posthog-python#939), review bots on the Python PR surfaced three hardening gaps that apply to this SDK too. The Python port shipped the fixes; this PR backports them so the two SDKs behave the same.

Changes

  1. matchesExtraSchema — a declared integer extra accepted any number, fractional ones included (3.5), because typeof cannot separate int from float. It now requires Number.isInteger per JSON Schema semantics: 3 and 3.0 pass, 3.5 stays out of extras and the captured properties (raw keeps it for the handler).
  2. handleFeedback — a thrown onFeedback handler was logged with its full message. A handler can echo the unsanitized report (PII, credentials, log-forging newlines) into its error message, so the log now carries only the error name, matching the report log beside it.
  3. prepareToolCall — every call named like the feedback tool was flagged isFeedback, so a dispatcher following the documented if (prepared.isFeedback) ... return flow suppressed a real tool that collides with the name. A host-supplied options.originalTool (already flowing in for model ownership) is stateless proof a real application tool owns the name — the virtual tool never exists in the host's own list — so it now wins and the real tool dispatches normally. Without originalTool the behavior is unchanged.

How did you test this code?

Three new tests in feedback.test.ts (fractional vs whole integer extras across capture + handler surfaces, thrown-handler log carries no error text, originalTool collision disambiguation), plus a logger assertion added to the existing thrown-handler test. Full @posthog/mcp unit suite: 48 files, 876 tests, lint and format clean.


Created with PostHog Desktop

… echo, and dispatcher shadowing

- matchesExtraSchema: a declared "integer" extra accepted any number,
  fractional ones included (3.5); now requires a whole value per JSON
  Schema semantics.
- handleFeedback: a thrown onFeedback was logged with its full message,
  which can echo agent-controlled report text (PII, credentials,
  log-forging newlines) into host logs; now logs only the error name,
  matching the report log beside it.
- prepareToolCall: every call named like the feedback tool was flagged
  as feedback, so a dispatcher following the documented flow suppressed
  a real colliding tool; a host-supplied originalTool is stateless proof
  a real tool owns the name and now wins.

Backports the hardening found while porting the feature to
posthog-python (PostHog/posthog-python#939).

Generated-By: PostHog Desktop
Task-Id: 0b5063cb-6fcf-4364-be5e-de945b1448f0
@gesh
gesh marked this pull request as ready for review September 11, 2026 13:17
@gesh
gesh requested review from a team as code owners September 11, 2026 13:17
@gesh
gesh removed request for a team September 11, 2026 13:20
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Security Review

The error-log mitigation remains incomplete because a thrown Error’s mutable name is logged verbatim, allowing report-derived text to reach host logs if a handler assigns it there.

Prompt To Fix All With AI
### Issue 1
packages/mcp/src/extensions/feedback.ts:355-356
**Exception name remains unsafe**

The handler’s exception name is still interpolated without sanitization. Because `Error.name` is writable, a handler can assign report-derived PII or newline characters to it before throwing, allowing the same log disclosure this change intends to prevent. Log a fixed label or an allowlisted built-in type instead.

**How this was verified:** `onFeedback` receives the agent-controlled report, while `handleFeedback` passes a thrown Error’s mutable `name` directly to the unsanitized host logger.

```suggestion
      logger('Warning: onFeedback handler threw; returning the default acknowledgement')
```

### Issue 2
packages/mcp/src/extensions/posthog-mcp.ts:313
**Wrong absence sentinel**

The new `options.originalTool == null` condition treats `null` as an absence sentinel. This violates the packages/mcp requirement that optional values use `undefined`, so the check must be explicit before merging.

```suggestion
      this.#feedbackOptions !== undefined && name === this.#feedbackToolName && options.originalTool === undefined
```

### Issue 3
.changeset/mcp-feedback-hardening.md:5
**Changeset is too detailed**

This changeset gives a long implementation-level explanation of all three mechanisms. The release guide requires one short, user-facing line that states the fix or feature, so the implementation details must be moved to the PR description before merging.

```suggestion
Harden feedback validation, error logging, and tool-name collision handling.
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(mcp): harden send_feedback against i..." | Re-trigger Greptile

Comment thread packages/mcp/src/extensions/feedback.ts Outdated
Comment thread packages/mcp/src/extensions/posthog-mcp.ts
Comment thread .changeset/mcp-feedback-hardening.md Outdated
Comment thread packages/mcp/src/extensions/posthog-mcp.ts
@gesh

gesh commented Sep 11, 2026 •

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: router (cheap-first pass) + narrow validation as needed

Verdict: ✅ APPROVE (round 4 @ 382e084)

The custom-dispatcher examples now pass the original application descriptor, and the regression test uses the same contract. The virtual-descriptor concern does not apply because originalTool must come from the application list before PostHog preparation.

Key findings

No open findings.

Convergence

All inline QA threads are resolved. The focused feedback suite and MCP lint pass.

Reviewer summaries

Reviewer Assessment
🧭 triage The application-ownership contract is explicit in examples, documentation, and tests.
Previous rounds (3)

round 3 @ 8fd8198 — REQUEST CHANGES: two dispatcher contract findings remained.
round 2 @ fd632f1 — REQUEST CHANGES: unsafe error-name logging and uncertain descriptor ownership.
round 1 @ fd632f1 — APPROVE WITH NITS: update the custom-dispatcher examples.


Automated by QA Swarm — not a human review

@gesh gesh left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note

🤖 Automated comment by QA Swarm — not written by a human

QA Swarm review complete. See inline comments.

Comment thread packages/mcp/src/extensions/feedback.ts Outdated
Comment thread packages/mcp/src/extensions/posthog-mcp.ts
Generated-By: PostHog Desktop
Task-Id: 9a0c57df-9c72-446a-acb4-cb319a85d5d7
Generated-By: PostHog Desktop
Task-Id: 9a0c57df-9c72-446a-acb4-cb319a85d5d7
@gesh gesh added the stamphog label Sep 11, 2026 — with PostHog
Generated-By: PostHog Desktop
Task-Id: 9a0c57df-9c72-446a-acb4-cb319a85d5d7
@gesh
gesh merged commit a5c1182 into main Sep 14, 2026
62 checks passed
@gesh
gesh deleted the posthog/mcp-feedback-hardening branch September 14, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants