Skip to content

fix(mcp): honest list filters/contract + meta.total guard + page-size cap + stop leaking errors + polish - #276

Merged
dodeja merged 5 commits into
mainfrom
fix/mcp-server-list-and-polish
Jul 1, 2026
Merged

fix(mcp): honest list filters/contract + meta.total guard + page-size cap + stop leaking errors + polish#276
dodeja merged 5 commits into
mainfrom
fix/mcp-server-list-and-polish

Conversation

@dodeja

@dodeja dodeja commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the MCP list_* tools honest about what they can answer, stops leaking internal error detail to clients, and polishes search_container / get_supported_shipping_lines / the query-guidance resource. AI-drafted PR for human review.

List contract honesty

  • buildListContract is now filter-aware. An unfiltered firehose no longer claims can_answer:["which records match filters"]; it tells the agent it needs a scoping filter. A filtered call reports "which records match the applied filters".
  • Echoes dropped/unsupported filters — prefers the SDK's unsupportedFilters when present, otherwise derives them from the per-entity supported vocabulary (status/port/carrier/updated_after; tracking_request: status/request_type/filters) so the agent is never told a phantom filter applied.
  • meta.total honesty: an unfiltered total above a plausibility threshold (admin-token firehose) is flagged total_is_reliable:false and the agent is warned not to quote it as the filtered worklist size.
  • Slims the repeated ~2KB column_catalog off every list response — it moved to a one-time MCP resource (terminal49://docs/list-display-columns); contracts reference it via display.column_catalog_resource.
  • Enforces a page_size cap (clamp to 100) at the MCP Zod layer.

Stop leaking errors

  • Tool error path returns a generic message and logs the real error to stderr.
  • api/mcp.ts 500 path no longer returns error.data = err.message.

Polish

  • get_supported_shipping_lines hides the T49 Test Carrier (scac: TEST).
  • search_container derives a real status instead of blindly returning "unknown", and flags duplicate container numbers (duplicate_number) so the agent can disambiguate same-number results.
  • query-guidance: removed phantom filters (status=discharged, has_hold) and the non-functional demurrage.pickup_lfd sort path; aligned to the real filter vocabulary and explained client-side derivation/sorting.
  • Removed dead *Tool export objects in search-container and get-supported-shipping-lines.

Issues

Closes DEV-10658
Closes DEV-10663
Closes DEV-10665

Green gate

  • npm run build --workspace @terminal49/sdk — pass
  • npm run build --workspace @terminal49/mcp — pass
  • npm run type-check --workspace @terminal49/sdk — pass
  • npm run type-check --workspace @terminal49/mcp — pass
  • npm test --workspace @terminal49/sdk -- --run51 pass / 2 skip
  • npm run test --workspace @terminal49/mcp -- --run86 pass (77 baseline + 9 new TDD tests)

TDD: the 9 new tests were written failing first (in contracts.test.ts), then implemented to green.

Note: this is an AI-drafted PR and should be reviewed by a human before merge.

🤖 Generated with Claude Code

Greptile Summary

This PR makes the MCP list_* tool contracts honest about filter scope, stops error detail from leaking to clients, and polishes several tools (search_container, get_supported_shipping_lines) and the query-guidance resource.

  • List contract honesty: buildListContract now derives applied/dropped filter keys, flags an implausibly large unfiltered meta.total as unreliable, enforces a 100-row page_size cap via Zod transform, and moves the ~2KB column catalog to a one-time MCP resource.
  • Error hygiene: wrapToolWithContract now catches all tool errors and returns a generic message; api/mcp.ts 500 handler drops data: err.message from the JSON-RPC response body.
  • Tool polish: search_container derives a real container status from timestamp/availability signals and flags duplicate container numbers; get_supported_shipping_lines hides the T49 Test Carrier; query-guidance removes phantom filters and corrects field names.

Confidence Score: 3/5

The changes are broadly correct and well-tested, but a gap in isProvided causes list_tracking_requests({ filters: {} }) to report an incorrectly filtered contract, the opposite of what this PR aims to guarantee.

The isProvided helper treats an empty plain object as a provided value. For list_tracking_requests, 'filters' is in the supported-filter vocabulary, so passing filters: {} causes isFiltered = true, the contract claims 'which records match the applied filters', and totalIsReliable is forced true even for a firehose total. This directly undermines the PR's core goal. All other changes (error masking, column-catalog extraction, status derivation, duplicate-number flagging, TEST carrier hiding) look correct and are covered by the new TDD tests.

packages/mcp/src/server.ts (isProvided and buildListContract logic)

Important Files Changed

Filename Overview
packages/mcp/src/server.ts Core change: adds buildListContract with filter-honesty signals, page-size cap, and generic tool-error wrapping. isProvided doesn't handle empty objects, causing a false filtered signal when list_tracking_requests is called with filters: {}; also minor presentationGuidance text issue for zero-result pages.
packages/mcp/src/tools/search-container.ts Added determineContainerStatus and flagDuplicateContainerNumbers; both are correct. Minor: destination field reuses port_of_discharge_name instead of an inland-destination field, which misrepresents containers on rail.
api/mcp.ts Removed data: err.message from 500 response to stop leaking internal error detail. Correct and straightforward.
packages/mcp/src/resources/list-display.ts New resource exposing the column catalog as a one-time MCP resource; static data and no logic concerns.
packages/mcp/src/tools/contracts.test.ts Adds 9 TDD tests covering the new contract honesty signals, shipping-line hiding, status derivation, and duplicate-number flagging. Tests are well-scoped; the filters: {} false-positive case is not covered.
packages/mcp/src/resources/query-guidance.ts Removes phantom filters and corrects field names. Clean fix.
packages/mcp/src/tools/get-supported-shipping-lines.ts Adds isHiddenCarrier to filter out the T49 Test Carrier by SCAC and name with correct case-insensitive comparison.
packages/mcp/src/mcp.test.ts Reformatting plus updated assertions for the new list-display-columns resource and non-leaking error message. No logic concerns.
packages/mcp/tests/api-handler.test.ts Test infrastructure helpers only; no functional changes visible in the diff.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Agent calls list_* tool] --> B[Zod validates & clamps page_size <= 100]
    B --> C[executeList* fetches from Terminal49 API]
    C --> D{Error?}
    D -- Yes --> E[wrapToolWithContract catches\nLogs real error to stderr\nReturns generic message to agent]
    D -- No --> F[buildListContract]
    F --> G{appliedFilterKeys}
    G -- filters provided --> H[isFiltered = true]
    G -- no filters --> I[isFiltered = false\nrequires_more_data: needs filter]
    F --> J{meta.total check}
    J -- unfiltered & total > 1000 --> K[total_is_reliable = false]
    J -- filtered OR total <= 1000 --> L[total_is_reliable = true]
    F --> M{dropped filters?}
    M -- unsupported --> N[dropped_filters echoed]
    F --> O[ResponseContract attached]
    O --> P[Agent receives honest contract]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Agent calls list_* tool] --> B[Zod validates & clamps page_size <= 100]
    B --> C[executeList* fetches from Terminal49 API]
    C --> D{Error?}
    D -- Yes --> E[wrapToolWithContract catches\nLogs real error to stderr\nReturns generic message to agent]
    D -- No --> F[buildListContract]
    F --> G{appliedFilterKeys}
    G -- filters provided --> H[isFiltered = true]
    G -- no filters --> I[isFiltered = false\nrequires_more_data: needs filter]
    F --> J{meta.total check}
    J -- unfiltered & total > 1000 --> K[total_is_reliable = false]
    J -- filtered OR total <= 1000 --> L[total_is_reliable = true]
    F --> M{dropped filters?}
    M -- unsupported --> N[dropped_filters echoed]
    F --> O[ResponseContract attached]
    O --> P[Agent receives honest contract]
Loading

Comments Outside Diff (1)

  1. packages/mcp/src/tools/search-container.ts, line 212-214 (link)

    P2 Both pod_terminal and destination are set to attrs.port_of_discharge_name. For sea-only containers this is a reasonable proxy, but for inland/rail containers the POD is a transshipment point, not the final destination. The field destination is silently wrong for those records — agents reading it will report the sea-port, not the inland ramp. Consider using attrs.destination_name or attrs.final_destination_name for destination, falling back to POD only when no inland destination is available.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/mcp/src/tools/search-container.ts
    Line: 212-214
    
    Comment:
    Both `pod_terminal` and `destination` are set to `attrs.port_of_discharge_name`. For sea-only containers this is a reasonable proxy, but for inland/rail containers the POD is a transshipment point, not the final destination. The field `destination` is silently wrong for those records — agents reading it will report the sea-port, not the inland ramp. Consider using `attrs.destination_name` or `attrs.final_destination_name` for `destination`, falling back to POD only when no inland destination is available.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/mcp/src/server.ts:710-711
**`isProvided` treats an empty object as a provided filter**

`isProvided` only guards against `undefined`, `null`, and `''`. An empty plain object `{}` passes the check. For `list_tracking_requests`, the supported-filter list is `['status', 'request_type', 'filters']`, where `'filters'` refers to the raw API-filter pass-through arg. An agent that sends `{ filters: {} }` will have `isProvided(args.filters)``true`, so `applied = ['filters']` and `isFiltered = true`. The resulting contract then claims "which records match the applied filters" — the exact dishonesty this PR aims to eliminate — even though no API-level filter was actually sent. The `totalIsReliable` check is also defeated: a firehose total will be flagged `total_is_reliable: true` because `isFiltered` is true.

### Issue 2 of 3
packages/mcp/src/tools/search-container.ts:212-214
Both `pod_terminal` and `destination` are set to `attrs.port_of_discharge_name`. For sea-only containers this is a reasonable proxy, but for inland/rail containers the POD is a transshipment point, not the final destination. The field `destination` is silently wrong for those records — agents reading it will report the sea-port, not the inland ramp. Consider using `attrs.destination_name` or `attrs.final_destination_name` for `destination`, falling back to POD only when no inland destination is available.

```suggestion
    pod_terminal: toTextOrUndefined(attrs.port_of_discharge_name),
    pol_terminal: toTextOrUndefined(attrs.port_of_lading_name),
    destination: toTextOrUndefined(
      attrs.destination_name ??
      attrs.final_destination_name ??
      attrs.port_of_discharge_name,
    ),
```

### Issue 3 of 3
packages/mcp/src/server.ts:814-826
**`presentationGuidance` says "single result" when `count === 0`**

When `count === 0`, the condition `count <= 1` is true, so guidance reads "For a single result, provide a concise row summary." This contradicts an empty list and is confusing for agents that may try to display a summary for a result that doesn't exist. The `empty_state` string in `display` is correct, but the `presentation_guidance` string contradicts it.

Reviews (1): Last reviewed commit: "fix(mcp): honest list filters/contract +..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@linear-code

linear-code Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

DEV-10658

DEV-10663

DEV-10665

@vercel

vercel Bot commented Jun 24, 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, Comment Jul 1, 2026 8:47pm

Request Review

@dodeja
dodeja marked this pull request as ready for review June 26, 2026 00:24

@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: 6d32bbda48

ℹ️ 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".

Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/server.ts
dodeja added a commit that referenced this pull request Jun 26, 2026
…arden status derivation

Address PR #276 review:
- isProvided() now treats an empty array/plain object as not-provided, so a
  raw `{ filters: {} }` pass-through no longer marks list_tracking_requests as
  filtered (which falsely trusted the firehose meta.total).
- buildListContract presentation guidance no longer claims "single result"
  when the list is empty; it points agents at the empty_state hint instead.
- determineContainerStatus is defensive against missing/oddly-typed `attrs`
  from the lightweight /search payload (an unverified shape) and documents
  that `unknown` is the honest fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dodeja

dodeja commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Triaged the automated review (Codex + Greptile) and pushed c46eed1.

Addressed:

  • Empty raw filter treated as scoped (Greptile P1 / Codex P2, server.ts:710) — isProvided now returns false for an empty array or empty plain object. A raw { filters: {} } on list_tracking_requests (the only entity whose vocabulary includes the filters pass-through) no longer flips isFiltered to true, so the contract stops claiming "which records match the applied filters" and stops marking the firehose meta.total reliable. Regression test added.
  • "single result" guidance when count === 0 (Greptile P2, server.ts:826) — presentation guidance now has a distinct empty-list branch that points agents at empty_state instead of the single-row text. Regression test added.
  • Known residual: search_container status derivationdetermineContainerStatus is now defensive against missing/oddly-typed attrs (the /search payload shape for these lifecycle fields is unverified): guards non-object input, ignores null/empty/'false' signals, and a code comment documents that unknown is the honest fallback when the lightweight search payload omits these fields.

Intentionally skipped: none — the two distinct bot findings (Greptile P1 and Codex P2 are the same empty-object issue) and the residual are all addressed.

Gate green from the worktree: SDK build/type-check/test (51 pass, 2 skip), MCP build/type-check/test (88 pass). No SDK public-surface change, so no docs regeneration. oxfmt run only on the three changed files.

@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: c46eed1579

ℹ️ 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".

Comment thread packages/mcp/src/server.ts
dodeja added a commit that referenced this pull request Jun 26, 2026
…s filters

The list_tracking_requests tool exposes a raw `filters` pass-through that the
SDK copies verbatim into the query string. A caller could smuggle
`page[size]`/`page[number]` through it and bypass the MAX_LIST_PAGE_SIZE cap that
the dedicated `page`/`page_size` schema enforces. Strip those keys before
building the SDK filter object so pagination is owned exclusively by the capped
schema. Adds a regression test.

Addresses Codex review comment on PR #276.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dodeja

dodeja commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Triaged the automated review comments (Codex + Greptile) against the current tip (c46eed1).

Addressed (1):

  • P2 (Codex): raw page[size] overrides bypass the page-size caplist_tracking_requests exposes a raw filters pass-through that the SDK copies verbatim into the query, so a caller could send filters: { "page[size]": "10000" } and slip past MAX_LIST_PAGE_SIZE. Now stripping page[size]/page[number] from the raw filters before the SDK call so pagination is owned exclusively by the capped page/page_size schema. Added a regression test. (commit f4c5096)

Skipped — already fixed in c46eed1 (these comments were filed against the earlier 6d32bbd):

  • P1 (Greptile) isProvided treats an empty object as a provided filter — already fixed: isProvided now returns false for empty arrays/objects.
  • P2 (Codex) "Treat empty raw filters as unscoped" — same fix; { filters: {} } no longer marks the list as filtered.
  • P2 (Greptile) presentationGuidance says "single result" when count === 0 — already fixed: a dedicated count === 0 branch now points at the empty-state guidance.

Known residual (already resolved at tip):

  • search_container status derivation vs the live /search payload — c46eed1 already made determineContainerStatus defensive (guards missing/oddly-typed attrs via isTruthySignal, prefers explicit attrs.status, falls back to unknown) and documents that the /search field presence is unverified. No further change needed.

Green gate from a fresh worktree off the PR tip: SDK build/type-check/test (51 pass, 2 skip) and MCP build/type-check/test (89 pass) all green. No SDK public surface changed, so no docs regen. Formatting kept to the changed logical lines only (oxfmt is not clean on this file on main, so I avoided mass reformat churn).

@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: f4c5096afd

ℹ️ 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".

Comment thread packages/mcp/src/server.ts Outdated

@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: 1d3f902b05

ℹ️ 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".

Comment thread packages/mcp/src/server.ts Outdated
> = {
container: ['status', 'port', 'carrier', 'updated_after'],
shipment: ['status', 'port', 'carrier', 'updated_after'],
tracking_request: ['status', 'request_type', 'filters'],

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 Stop treating request_type as a supported filter

In a list_tracking_requests({ request_type: 'container' }) call, this new supported-filter vocabulary makes buildListContract report applied filters and trust meta.total. The OpenAPI source of truth for GET /tracking_requests does not define filter[request_type], while executeListTrackingRequests just forwards that unsupported key, so a successful response can still be unscoped and agents may present an account-wide list as request-type filtered. Either map this to a real API filter or report it as dropped.

Useful? React with 👍 / 👎.

Comment thread packages/mcp/src/server.ts Outdated
dodeja and others added 5 commits July 1, 2026 13:41
… cap + stop leaking errors + polish

Make the list_* tools honest about what they can answer and stop leaking
internal error detail to MCP clients.

List contract (DEV-10658, DEV-10665):
- buildListContract is now filter-aware. An unfiltered firehose no longer
  claims can_answer:["which records match filters"]; instead it tells the
  agent it needs a scoping filter. A filtered call reports "which records
  match the applied filters".
- Echo dropped/unsupported filters: prefers the SDK's unsupportedFilters when
  present, otherwise derives them from the per-entity supported vocabulary
  (status/port/carrier/updated_after; tracking_request: status/request_type/
  filters) so the agent is never told a phantom filter applied.
- meta.total honesty: an unfiltered total above a plausibility threshold
  (admin-token firehose) is flagged total_is_reliable:false and the agent is
  warned not to quote it as the filtered worklist size.
- Slim the repeated ~2KB column_catalog off every list response. It moved to
  a one-time MCP resource (terminal49://docs/list-display-columns); contracts
  now reference it via display.column_catalog_resource.
- Enforce a page_size cap (clamp to 100) at the MCP Zod layer.

Stop leaking errors (DEV-10663):
- Tool error path returns a generic message and logs the real error to stderr.
- api/mcp.ts 500 path no longer returns error.data = err.message.

Polish (DEV-10665):
- get_supported_shipping_lines hides the T49 Test Carrier (scac TEST).
- search_container derives a real status instead of blindly returning
  "unknown", and flags duplicate container numbers (duplicate_number) so the
  agent can disambiguate same-number results.
- query-guidance: removed phantom filters (status=discharged, has_hold) and
  the non-functional demurrage.pickup_lfd sort path; aligned to the real
  filter vocabulary and explained client-side derivation/sorting.
- Removed dead *Tool export objects in search-container and
  get-supported-shipping-lines.

Green gate: SDK 51 pass/2 skip; MCP 86 pass (77 baseline + 9 new TDD tests).

Closes DEV-10658
Closes DEV-10663
Closes DEV-10665

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arden status derivation

Address PR #276 review:
- isProvided() now treats an empty array/plain object as not-provided, so a
  raw `{ filters: {} }` pass-through no longer marks list_tracking_requests as
  filtered (which falsely trusted the firehose meta.total).
- buildListContract presentation guidance no longer claims "single result"
  when the list is empty; it points agents at the empty_state hint instead.
- determineContainerStatus is defensive against missing/oddly-typed `attrs`
  from the lightweight /search payload (an unverified shape) and documents
  that `unknown` is the honest fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s filters

The list_tracking_requests tool exposes a raw `filters` pass-through that the
SDK copies verbatim into the query string. A caller could smuggle
`page[size]`/`page[number]` through it and bypass the MAX_LIST_PAGE_SIZE cap that
the dedicated `page`/`page_size` schema enforces. Strip those keys before
building the SDK filter object so pagination is owned exclusively by the capped
schema. Adds a regression test.

Addresses Codex review comment on PR #276.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
executeListTrackingRequests strips raw page[size]/page[number] from the
nested `filters` bag before the SDK call, but buildListContract was still
fed the original args. A `list_tracking_requests({ filters: { 'page[size]':
'10000' } })` request is unfiltered after sanitization, yet the contract saw
a non-empty `filters` arg, reported applied filters, and flagged the
account-wide meta.total as reliable — letting agents present an unscoped
firehose as a scoped worklist.

Sanitize the filters context (same page-key stripping) before building the
contract so it reflects the request the API actually saw. An emptied
`filters` bag is treated as unprovided by isProvided; genuine filters remain
scoped. Adds contract tests for both cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-up Greptile P2 findings on the list-contract-honesty work:

- request_type has no filter[request_type] in the GET /tracking_requests
  OpenAPI source of truth, so buildListContract no longer treats it as a
  scoping filter (it now falls through to dropped/unsupported filters
  instead of falsely marking the list as filtered and meta.total reliable).
- The raw `filters` pass-through bag is only treated as applied when it
  contains a real filter[...] key; a bag of only non-filter knobs (e.g.
  `{ include: 'tracked_object' }`) no longer counts as scoping the list.

Adds contract tests covering both cases.
@dodeja
dodeja force-pushed the fix/mcp-server-list-and-polish branch from 1d3f902 to 509f820 Compare July 1, 2026 20:46
@dodeja

dodeja commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (resolved conflicts with the MCP spec-adherence work in #281 — completions/completable import, resource-link content on list_containers, the get-container default-includes/format change from #277 — merged both sides where they overlapped) and addressed the two outstanding review findings:

Fixed

  • request_type falsely treated as a scoping filter (P2): GET /tracking_requests has no filter[request_type] in the OpenAPI source of truth, so a bare request_type arg couldn't actually scope the list even though it forwards fine. Removed it from the tracking_request supported-filter vocabulary in buildListContract; it now falls through to dropped_filters instead of falsely marking the list filtered / meta.total reliable.
  • Raw filters pass-through counted non-filter keys as scoping (P2): { filters: { include: 'tracked_object' } } previously counted as an applied filter. appliedFilterKeys now only treats the raw filters bag as scoping when it contains a real filter[...] key (filter[request_number], filter[status], filter[scac], filter[created_at][...], filter[updated_at][...]).
  • Added contract tests for both cases (contracts.test.ts).

Already resolved on this branch (verified, no action needed): isProvided treating {} as provided, presentation_guidance claiming "single result" on count === 0, raw page[size]/page[number] bypassing the pagination cap, and the contract being built from unsanitized tracking filters — all four were fixed in earlier commits (c46eed1, f4c5096, 1d3f902) with matching tests already in place.

Green gate (after npm ci + npm run build --workspace @terminal49/sdk):

  • npm run test --workspace @terminal49/mcp -- --run — 146 pass
  • npm run build --workspace @terminal49/mcp — pass
  • npm run lint --workspace @terminal49/mcp — pass
  • npx tsc --noEmit -p tsconfig.json (api/) — pass
  • npm run test --workspace @terminal49/sdk -- --run — 61 pass / 2 skip

@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: 509f820d09

ℹ️ 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".

Comment on lines +157 to +158
container: ['status', 'port', 'carrier', 'updated_after'],
shipment: ['status', 'port', 'carrier', 'updated_after'],

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 Stop marking dropped list filters as applied

When list_containers or list_shipments is called with status, port, carrier, or updated_after, these entries make appliedFilterKeys treat the response as filtered and total_is_reliable as true. I checked the SDK query builder, and it explicitly reports status/port/carrier/updatedAfter as unsupported for /containers and /shipments instead of forwarding them, so this scenario returns an unfiltered list while the contract tells agents they can answer “which records match the applied filters” and safely quote meta.total.

Useful? React with 👍 / 👎.

@dodeja
dodeja merged commit fb35b3b into main Jul 1, 2026
11 checks passed
@dodeja
dodeja deleted the fix/mcp-server-list-and-polish branch July 1, 2026 23:02
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.

1 participant