Skip to content

fix(mcp): harden tool output quality and coverage - #337

Merged
dodeja merged 8 commits into
mainfrom
cursor/improve-mcp-quality-17d9
Aug 21, 2026
Merged

fix(mcp): harden tool output quality and coverage#337
dodeja merged 8 commits into
mainfrom
cursor/improve-mcp-quality-17d9

Conversation

@dodeja

@dodeja dodeja commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add real MCP client-transport happy-path and redacted-error coverage for all 10 public tools, with realistic tool-specific fixture assertions
  • exercise all prompts and resources through the transport; preserve m / ma carrier completions and full tool/prompt/resource surfaces on protocols 2026-07-28 and 2025-11-25
  • distinguish three track_container outcomes without leaking internals: no request created, request created but details pending, and existing match temporarily unavailable
  • sanitize exception messages before Sentry capture as well as in client responses/logs
  • advertise only list filters the Terminal49 API actually honors: shipment number and tracking_stopped; container lists now state that server-side operational filters are unavailable
  • reduce default list cost to 25 rows, omit shipment relationships from list results unless requested, and request raw-only route data
  • enforce consistency between live titles/annotations and both store-listing files, including locked listing values and reviewer-case shape

Measurement

  • default list response ceiling is now 25 rows instead of the upstream/default-dependent size (the explicit maximum remains 100)
  • list_shipments no longer includes nested containers by default; callers can opt in with include_containers: true
  • route calls request one raw representation instead of format: both, avoiding construction of a discarded mapped payload
  • transport coverage calls every public tool on both a realistic happy path and a URL/token-bearing upstream failure path; each successful call is validated by the real MCP client against its advertised output schema
  • unsupported container/shipment filters can no longer be selected from the public tool schema or counted as applied by _response_contract

Verification

  • npm run test --workspace @terminal49/mcp -- --run — 15 files, 212 tests passed
  • npm run build --workspace @terminal49/mcp — passed
  • npm run lint --workspace @terminal49/mcp — passed under pinned Node 24.4.1
  • npx tsc --noEmit -p tsconfig.json — API gateway typecheck passed
  • git diff --check origin/cursor/fix-list-output-schema-0c41...HEAD — passed

Left unchanged

  • stacked on fix(mcp): validate list tool SDK sidecars #336, which owns the unsupportedFilters schema correction for list_containers and list_shipments; this PR does not duplicate it
  • carrier completions remain uncached because the production HTTP gateway is stateless; a per-server cache has no effect across requests, while a module-wide cache could contaminate separate account/backend clients
  • preserves MCP SDK v2, OAuth behavior, the locked listing name/tagline/URL/ChatGPT one-liner, the 9-read/1-write annotation contract, and the tight 10-tool catalog
  • no live reviewer fixture is assumed, no app-store submission is performed, and no production secrets are changed

Requirements verified against

Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Greptile Summary

The PR hardens the MCP tool surface by correcting advertised list filters, bounding default list sizes, reducing route and relationship payloads, clarifying tracking outcomes, sanitizing reported errors, and adding transport-level contract coverage.

  • Updates container, shipment, tracking-request, and route tool behavior for smaller and more truthful responses.
  • Distinguishes uncreated, pending, and temporarily unavailable tracking outcomes.
  • Adds realistic client-transport tests across all ten public tools and locks store-listing metadata against the live server.
  • Introduces one non-blocking observability concern because Sentry sanitization discards original exception diagnostics.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking observability issue in how sanitized exceptions are reported to Sentry.

Runtime contracts and list behavior are consistent with the SDK paths examined, but replacing each reported exception with a new generic Error removes the original stack and cause needed for effective incident diagnosis.

Files Needing Attention: packages/mcp/src/sentry.ts

Important Files Changed

Filename Overview
packages/mcp/src/server.ts Aligns public schemas and response contracts with supported filters, bounded pagination, and distinct tracking states.
packages/mcp/src/tools/track-container.ts Preserves created and existing-match states when linked container details temporarily return not found.
packages/mcp/src/sentry.ts Prevents secret-bearing messages from reaching Sentry but also removes the original exception stack and cause.
packages/mcp/src/tools/list-shipments.ts Replaces ignored filters with supported shipment filters and defaults nested containers off.
packages/mcp/src/tools/list-containers.ts Stops forwarding unsupported operational filters and enforces the smaller default page size.
packages/mcp/src/tool-transport.test.ts Adds realistic transport-level success, schema-validation, and redacted-failure coverage for all public tools.

Sequence Diagram

sequenceDiagram
  participant C as MCP Client
  participant S as MCP Server
  participant K as Terminal49 SDK
  participant A as Terminal49 API
  participant E as Sentry
  C->>S: Call public tool
  S->>K: Invoke SDK operation
  K->>A: API request
  alt Successful response
    A-->>K: Raw or mapped data
    K-->>S: Curated result
    S-->>C: Structured content and response contract
  else Tool exception
    A-->>K: Error
    K-->>S: Exception
    S->>E: Generic replacement Error
    S-->>C: Redacted tool error
  end
Loading

Fix all with Greploop Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
packages/mcp/src/sentry.ts:97-104
**Generic errors discard diagnostics**

`captureMcpException` replaces every caught tool exception with a new generic `Error`, so Sentry loses the original call-site stack and cause chain and groups distinct upstream failures at this sanitizer, making production incidents harder to diagnose.

---

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): advertise only supported list ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Aug 21, 2026 1:04pm

Request Review

Comment on lines +97 to +104
const safeError = new Error(
'The Terminal49 upstream request could not be completed.',
);
const name = error instanceof Error ? error.name : 'Error';
safeError.name = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(name)
? name
: 'Error';
Sentry.captureException(safeError);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Generic errors discard diagnostics

captureMcpException replaces every caught tool exception with a new generic Error, so Sentry loses the original call-site stack and cause chain and groups distinct upstream failures at this sanitizer, making production incidents harder to diagnose.

Knowledge Base Used: MCP Server Core (@terminal49/mcp)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/mcp/src/sentry.ts
Line: 97-104

Comment:
**Generic errors discard diagnostics**

`captureMcpException` replaces every caught tool exception with a new generic `Error`, so Sentry loses the original call-site stack and cause chain and groups distinct upstream failures at this sanitizer, making production incidents harder to diagnose.

**Knowledge Base Used:** [MCP Server Core (`@terminal49/mcp`)](https://app.greptile.com/terminal49/-/custom-context/knowledge-base/terminal49/api/-/docs/mcp-server-core.md)

---

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

Fix in Codex Fix in Claude Code

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80da54d283

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

carrier: z.string().optional().describe('Filter by shipping line SCAC'),
updated_after: z
.string()
number: z.string().optional().describe('Filter by shipment number'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clarify that shipment-number filtering excludes containers

When a user asks for a shipment using a container number, this description can lead the model to pass that identifier here and report an empty result. The OpenAPI source explicitly defines number as the original tracking request number (typically a master bill of lading or booking number) and states that it does not match container numbers (docs/openapi.json:604-605); describe that limitation and direct container-number lookups to search_container so valid shipments are not missed.

AGENTS.md reference: AGENTS.md:L23-L28

Useful? React with 👍 / 👎.

@vorflux vorflux 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.

Summary

Reviewed — found 5 issues in the MCP tool-output hardening changes. I reviewed the tool response contracts, tracking-request error paths, Sentry exception handling, list-tool schemas, and corresponding public documentation.

Findings

packages/mcp/src/tools/track-container.ts

  1. The linked-but-not-readable response is classified as pending and contradicts its own metadata.
  2. All upstream 404 errors are converted into successful invalid-input responses.

packages/mcp/src/sentry.ts

  1. Replacing the captured exception removes the original failure location and useful diagnostic context.

packages/mcp/src/server.ts

  1. The changed list-tool schemas leave the public MCP documentation advertising removed filters and stale defaults.
  2. Container-list contracts still recommend filters even though that endpoint now declares no supported filters.

Verdict

⚠️ Changes requested. The response contracts can mislead callers about tracking state and available filtering, while the new error handling obscures both upstream failures and Sentry diagnostics.


Review with Vorflux

Comment on lines +324 to +332
return {
tracking_request_created: true,
infer_result: infer,
tracking_request: {
request_number: number,
number_type: inferredNumberType,
scac: requestedScac || heuristicScac,
container_id: containerId,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This response explicitly contains tracking_request.container_id, so the request is already linked. Because it has no top-level id, buildTrackContract() classifies it as pending and emits that container linking is not immediate, contradicting both this payload and _metadata.presentation_guidance, which says it was created and linked. Preserve an explicit linked-but-details-unavailable state, return the linked ID in the shape the contract checks, or update buildTrackContract() to recognize tracking_request.container_id.

Comment on lines +360 to +379
if (isNotFound(error)) {
logMcpEvent({
event: 'tracking_request.not_found',
number,
numberType: inferredNumberType,
scac: requestedScac || heuristicScac,
duration_ms: duration,
timestamp: new Date().toISOString(),
});
return {
error: 'NotFound',
message:
'No tracked container matched this number, and Terminal49 could not create a tracking request for it. Verify the number and carrier SCAC, then retry.',
tracking_request_created: false,
_metadata: {
presentation_guidance:
'Clearly state that no tracking request was created. Ask the user to verify the identifier and carrier; do not imply that tracking is pending.',
recommendations: ['get_supported_shipping_lines', 'search_container'],
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This converts every NotFoundError from createTrackingRequestFromInfer() or the direct-create fallback into a successful MCP result claiming the number/carrier could not be resolved. However, the OpenAPI contract documents invalid tracking inputs as 422; a 404 can instead mean the infer/create route is missing or the configured upstream base URL is wrong. In that deployment/configuration scenario, callers receive isError: false, are told to correct valid input, and the operational failure bypasses the outer error handler. Only translate a specifically identified domain “number not found” response here; otherwise rethrow the 404 as an upstream/tool error.

Comment on lines +97 to +104
const safeError = new Error(
'The Terminal49 upstream request could not be completed.',
);
const name = error instanceof Error ? error.name : 'Error';
safeError.name = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(name)
? name
: 'Error';
Sentry.captureException(safeError);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Replacing every exception with a newly constructed Error discards the original stack, cause, HTTP status, and sanitized SDK details. The new stack always points to captureMcpException(), so failures with the same error name become effectively indistinguishable in Sentry. Since stderr logging also redacts message, operators no longer have enough context to identify the failing request path or cause. Redact sensitive fields in a Sentry beforeSend hook, or construct a sanitized exception that preserves safe stack frames and diagnostic tags such as status/tool/operation.

Comment on lines 1636 to +1647
inputSchema: z.object({
status: z.string().optional().describe('Filter by shipment status'),
port: z.string().optional().describe('Filter by POD port LOCODE'),
carrier: z.string().optional().describe('Filter by shipping line SCAC'),
updated_after: z
.string()
number: z.string().optional().describe('Filter by shipment number'),
tracking_stopped: z
.boolean()
.optional()
.describe('Filter by updated_at (ISO8601) >= value'),
.describe('Filter by whether shipping-line tracking has stopped'),
include_containers: z
.boolean()
.optional()
.default(false)
.describe(
'Include containers relationship in response. Default: true.',
'Include container relationships in each shipment. Default: false to keep list responses compact.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These input changes are not reflected in the published MCP documentation. docs/mcp/home.mdx and docs/api-docs/in-depth-guides/mcp.mdx still advertise status, port, carrier, and updated_after for both list tools and describe include_containers using the previous default. Users following those docs will pass fields that Zod strips before the handler, causing an unfiltered request without a dropped_filters warning. Update both documentation surfaces alongside this schema change, including the new number/tracking_stopped shipment filters and compact defaults.

Comment on lines +964 to +967
const filterGuidance =
supportedFilters.length > 0
? `a filter to scope this list (${supportedVocab})`
: 'server-side filters are not available for this list endpoint; use pagination and inspect returned rows';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For list_containers, supportedFilters is now empty, but later branches in this same contract still instruct the model to apply a filter before quoting a total and to try alternative filters or tighter date ranges when the page is empty. Those actions are impossible on this endpoint and contradict this new guidance. Condition those messages on supportedFilters.length > 0; for container lists, direct the model to paginate and qualify conclusions as page-local instead.

cursoragent and others added 8 commits August 21, 2026 13:02
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
@cursor
cursor Bot force-pushed the cursor/improve-mcp-quality-17d9 branch from 80da54d to c285883 Compare August 21, 2026 13:02
@dodeja
dodeja merged commit 1ea80e7 into main Aug 21, 2026
21 checks passed
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