Uh oh!
There was an error while loading. Please reload this page.
feat(mcp): MCP spec-adherence — server instructions, completions, resource links, content audience - #281
Conversation
…ource links, content audience
Additively bring the Terminal49 MCP closer to the MCP spec, matching the
existing stateless Streamable-HTTP server style. Four features:
1. Server instructions — createTerminal49McpServer now passes a second
ServerOptions arg with `instructions` (TERMINAL49_SERVER_INSTRUCTIONS): a
concise operating guide covering the ocean container/shipment domain, key
vocab (SCAC, BOL/booking, POL/POD, LFD, demurrage, holds, transport events),
the read tools vs the single write tool (track_container), and the canonical
chaining order. Advertised to the client at initialize.
2. Completions — the `track-shipment` prompt's `carrier` arg is wrapped with the
SDK's completable(); the completer is sourced live from
get_supported_shipping_lines, filtered by the partial value, returning SCAC
codes. Degrades to no suggestions on API error.
3. ResourceLinks — list_containers now appends MCP `resource_link` content
blocks (one per row) pointing at the registered terminal49://container/{id}
resource, so clients can resolve full detail on demand instead of paying for
it up front. README ResourceLinks claim updated to match reality.
4. Content audience annotations — steering-only payload (presentation guidance +
suggested follow-ups derived from _response_contract) is emitted as a
discrete text block annotated { audience: ['assistant'] } so clients can hide
it from end users; the answer block stays unannotated/user-visible.
Out of scope (stateless transport): resource subscriptions, progress, logging,
elicitation, sampling — tracked for a future RFC.
Tests: assert the server exposes `instructions`; the carrier completer returns
the expected filtered SCAC values (and empty on error); list_containers results
carry resource_link blocks with valid container URIs; the steering block carries
audience:['assistant']. Green gate: SDK 51 pass/2 skip, MCP 82 pass; both
type-check + build clean; changed files oxfmt/oxlint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The latest updates on your projects. Learn more about Vercel for GitHub.
|
…churn
Fix 1 (HIGH — completions never registered): the carrier prompt arg wired
`completable` around the OUTER ZodOptional. The MCP SDK unwraps ZodOptional
and checks isCompletable on the INNER ZodString when deciding whether to
advertise `completions` and register a completion handler, so the symbol on
the optional was missed and the capability was never advertised. Reorder to
`completable(z.string()...).optional()` so the inner string carries the
completion metadata; the SDK now advertises `completions` and registers the
completion/complete handler (verified via server.getCapabilities() and
_completionHandlerInitialized).
Fix 2 (HIGH — false-confidence test): the old completions test called the
completer function directly, bypassing MCP registration, so it passed even
though the capability was never advertised. Rewrite it to assert the
registered path: server.getCapabilities().completions is defined and
_completionHandlerInitialized is true (these FAIL against the pre-fix
outer-optional wiring and PASS after Fix 1). The value assertions
('m' -> ['MAEU','MSCU'], 'ma' -> ['MAEU']) are kept as a secondary unit
assertion run against the completer resolved off the inner string, the same
way the SDK registration keys off it.
Fix 3 (churn): the implementer's mass oxfmt reformat produced ~760 lines of
formatting churn in server.ts unrelated to the 4 features. Restored
server.ts to origin/main formatting and re-applied only the 4 substantive
edits (server instructions wiring, the completable carrier-arg change,
list_containers resource_link blocks, and the audience:['assistant']
steering annotation). server.ts diff vs origin/main is now 191 insertions /
9 deletions (down from 613 / 147). The other 3 features are intact.
Green gate: SDK+MCP build, type-check, and tests all pass (SDK 51 passed,
MCP 82 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>dodeja
commented
Jun 26, 2026
Addressed 2 HIGH review findings + stripped reformat churn (commit f2cc18d)Fix 1 — |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:f2cc18daa8
ℹ️ 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: completable( | ||
| z.string().describe('Shipping line SCAC code (e.g., MAEU for Maersk)'), | ||
| completeCarrierScac, | ||
| ).optional(), |
There was a problem hiding this comment.
Put the completer on the optional carrier schema
When a client requests completion/complete for the track-shipment prompt's optional carrier argument, this schema is a ZodOptional, not the completable string. The SDK uses the unwrapped inner schema to decide whether to register/advertise completions, but handlePromptCompletion later checks isCompletable(field) on the stored field itself, so this returns the empty completion result and never calls completeCarrierScac. Put the completer metadata on both the optional wrapper and the inner string, or provide a custom handler, so advertised carrier completions actually work.
Useful? React with 👍 / 👎.
| const content: ToolContent[] = buildContentPayload(result); | ||
| if (buildResourceLinks) { | ||
| content.push(...buildResourceLinks(result, args)); |
There was a problem hiding this comment.
Avoid sending full rows alongside ResourceLinks
When list_containers returns the normal SDK list result, this initializes content with buildContentPayload(result), which serializes the entire items page before the resource_link blocks are appended. For large pages or included relationships the tool still sends every full container row to the model and then adds links, so the new ResourceLinks path increases context instead of reducing it; omit or compact the full text payload when resource links are being emitted.
Useful? React with 👍 / 👎.
| function createCarrierScacCompleter( | ||
| client: Terminal49Client, | ||
| ): (value: string | undefined) => Promise<string[]> { | ||
| return async (value: string | undefined): Promise<string[]> => { | ||
| try { | ||
| const search = typeof value === 'string' ? value.trim() : ''; | ||
| const { shipping_lines } = await executeGetSupportedShippingLines({ search }, client); | ||
| return shipping_lines.slice(0, 100).map((line) => line.scac); | ||
| } catch { | ||
| return []; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Uncached live API call per completion keystroke
createCarrierScacCompleter calls executeGetSupportedShippingLines on every completion/complete request, which in turn calls client.shippingLines.list() — a full network round-trip each time. A user typing four characters to reach "MAEU" fires four sequential live fetches. The carrier list changes at most monthly, so a simple module-level TTL cache (e.g., 10–30 min) inside this function would eliminate the churn without any observable staleness. Without it, rapid autocompletion could also exhaust API rate limits.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/mcp/src/server.ts
Line: 760-771
Comment:
**Uncached live API call per completion keystroke**`createCarrierScacCompleter` calls `executeGetSupportedShippingLines` on every `completion/complete` request, which in turn calls `client.shippingLines.list()` — a full network round-trip each time. A user typing four characters to reach "MAEU" fires four sequential live fetches. The carrier list changes at most monthly, so a simple module-level TTL cache (e.g., 10–30 min) inside this function would eliminate the churn without any observable staleness. Without it, rapid autocompletion could also exhaust API rate limits.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| return { | ||
| type: 'resource_link', | ||
| uri: `${CONTAINER_RESOURCE_URI_PREFIX}${id}`, | ||
| name: number ? `Container ${number}` : `Container ${id}`, | ||
| description: | ||
| 'Compact container summary (status, milestones, holds, LFD) resolvable via resources/read.', | ||
| mimeType: 'text/markdown', | ||
| annotations: { audience: ['user', 'assistant'] }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
mimeType hardcoded; could drift from the container resource definition
buildContainerResourceLink sets mimeType: 'text/markdown' inline. The authoritative declaration lives in containerResource.mimeType in resources/container.ts, which also exports that constant. Importing from there would keep the resource_link's advertised MIME type in sync with the actual resource if it ever changes, rather than requiring a coordinated update in two places.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/mcp/src/server.ts
Line: 674-683
Comment:
**`mimeType` hardcoded; could drift from the container resource definition**`buildContainerResourceLink` sets `mimeType: 'text/markdown'` inline. The authoritative declaration lives in `containerResource.mimeType` in `resources/container.ts`, which also exports that constant. Importing from there would keep the resource_link's advertised MIME type in sync with the actual resource if it ever changes, rather than requiring a coordinated update in two places.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Additively brings the Terminal49 MCP (stateless Streamable HTTP,
packages/mcp) closer to the MCP spec, matching the existing server style. Four features, all done; tests included.1. Server instructions ✅
createTerminal49McpServernow passes a secondServerOptionsarg withinstructions(TERMINAL49_SERVER_INSTRUCTIONS, exported for tests). It's a concise (~200-word) LLM operating guide covering:track_container)search_container→get_container/get_shipment_details→get_container_transport_events(+get_container_route);get_supported_shipping_linesto resolve a carrier → SCACThe SDK advertises this in the
initializeresult.2. Completions ✅
The
track-shipmentprompt'scarrierargument is wrapped with the SDK'scompletable(). The completer is sourced live fromget_supported_shipping_lines, filtered by the partial value the user has typed, and returns SCAC codes (capped at the spec's 100). On API error it degrades gracefully to no suggestions rather than failing thecompletion/completerequest.This was the strongest target called out: a prompt arg that takes a carrier/SCAC, completed from
get_supported_shipping_lines(SCAC + name).3. ResourceLinks ✅ (and README claim corrected)
list_containersnow appends MCPresource_linkcontent blocks — one per container row — pointing at the already-registeredterminal49://container/{id}resource. Clients can render the compact list and resolve full per-container detail on demand viaresources/readinstead of paying for every container's full payload up front. The README's previously-false "ResourceLinks / 50-70% context reduction" claim is now accurate (moved tolist_containers; the misleading line onget_container_transport_eventswas corrected).Scoped to the cleanest single surface. Deferred: shipment ResourceLinks (
list_shipments) — there is noterminal49://shipment/{id}resource template registered yet, only the container template, so there is nothing valid to link to without adding a new resource. Noted in README.4. Content audience annotations ✅
Tool results now separate the human-readable answer from agent-steering metadata. The steering block (purpose + presentation guidance + suggested follow-up tools, derived from the
_response_contract) is emitted as a discretetextblock taggedannotations: { audience: ['assistant'] }so spec-aware clients can hide it from end users. The answer block (built bybuildContentPayload) is left unannotated and stays user-visible. Wired inwrapToolWithContract.Tests
Added to
packages/mcp/src/mcp.test.ts(using the existing(server as any)._registeredTools/_registeredPromptsprior art):instructions(and they cover SCAC/LFD/search_container/track_container)[]on API errorlist_containersresults containresource_linkblocks with validterminal49://container/{uuid}URIsaudience: ['assistant']while the answer block does notGreen gate
Run from worktree root — all green:
build(sdk + mcp): passtype-check(sdk + mcp): passoxfmt --check+oxlinton the changed files (server.ts,mcp.test.ts): cleanSDK public surface is unchanged (only
packages/mcptouched), so nodocs/sdk/referenceregeneration needed.oxfmt --writewas run only on the files I changed; because oxfmt was not previously clean onserver.ts, normalizing it produced reflow churn in that file (the net logical diff is ~200 lines).Out of scope — deferred to a future RFC
Resource subscriptions, progress notifications, server logging, elicitation, and sampling all require a stateful transport. The current deployment is stateless Streamable HTTP, so these are intentionally not implemented here and are tracked for a future RFC.
Merge coordination
server.tsand tool-result assembly also change in open PRs #276 and #280 (all branch offmain). This PR is deliberately additive (new helpers + new optionalwrapToolWithContractparam + new content blocks), but expect to coordinate the merge order to resolve overlap inserver.ts.🤖 Generated with Claude Code
Greptile Summary
This PR additively extends the Terminal49 MCP server with four MCP spec-adherence features: server-level
instructions(domain guide emitted at initialize), live SCAC completion on thetrack-shipmentprompt'scarrierargument,resource_linkcontent blocks onlist_containersresults, and assistant-only audience annotations on the steering content block that was previously embedded only instructuredContent.TERMINAL49_SERVER_INSTRUCTIONSis exported and passed asServerOptions.instructions; thecarrierarg wraps the innerz.string()withcompletable()before.optional(), which is the correct ordering for the SDK to detect and advertise thecompletionscapability.buildContainerResourceLinkandbuildListResourceLinksemitresource_linkblocks pointing atterminal49://container/{id}— the already-registered resource template — so clients can resolve full container detail on demand.wrapToolWithContractnow appends a separateTextContentblock taggedannotations: { audience: ['assistant'] }for every tool that has a response contract, separating steering hints from the user-visible answer block.Confidence Score: 4/5
Additive changes only; no existing tool behavior is broken, and the green gate (82 MCP tests, full type-check, lint) passed. The main thing to watch is that the steering content block is now appended to every contracted tool response, not just list_containers.
The implementation is correct and well-tested. The
completable()ordering (inner string first,.optional()after) is carefully validated by the new tests. Resource links point at the already-registeredterminal49://container/{id}template. The two findings are both non-blocking: the SCAC completer fires a live API call per keystroke with no caching, and themimeTypeon resource_link content is hardcoded rather than imported from the resource definition.packages/mcp/src/server.ts — the uncached carrier completer and the hardcoded mimeType in buildContainerResourceLink are both in this file.
Important Files Changed
completable()is correctly ordered before.optional(). No caching in the SCAC completer means one live API call per keystroke.(server as any)and internal module paths consistent with pre-existing patterns.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Client participant McpServer participant wrapToolWithContract participant executeListContainers participant buildListContract participant buildListResourceLinks participant buildSteeringContent Client->>McpServer: initialize McpServer-->>Client: "{instructions: TERMINAL49_SERVER_INSTRUCTIONS, capabilities: {completions: {}}}" Client->>McpServer: completion/complete (carrier arg partial) McpServer->>McpServer: createCarrierScacCompleter(value) McpServer->>McpServer: "executeGetSupportedShippingLines({search: value})" McpServer-->>Client: "[{MAEU, MSCU, ...}] filtered SCAC codes" Client->>McpServer: tools/call list_containers McpServer->>wrapToolWithContract: handler(args) wrapToolWithContract->>executeListContainers: execute(args, client) executeListContainers-->>wrapToolWithContract: "result {items: [...]}" wrapToolWithContract->>buildListContract: buildContract(result) buildListContract-->>wrapToolWithContract: contract wrapToolWithContract->>buildListResourceLinks: buildResourceLinks(result) buildListResourceLinks-->>wrapToolWithContract: "[resource_link{terminal49://container/{id}}, ...]" wrapToolWithContract->>buildSteeringContent: buildSteeringContent(contract) buildSteeringContent-->>wrapToolWithContract: "{type:text, annotations:{audience:[assistant]}}" wrapToolWithContract-->>McpServer: "{content: [answerBlock, ...resourceLinks, steeringBlock], structuredContent}" McpServer-->>Client: tool result%%{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"}}}%% sequenceDiagram participant Client participant McpServer participant wrapToolWithContract participant executeListContainers participant buildListContract participant buildListResourceLinks participant buildSteeringContent Client->>McpServer: initialize McpServer-->>Client: "{instructions: TERMINAL49_SERVER_INSTRUCTIONS, capabilities: {completions: {}}}" Client->>McpServer: completion/complete (carrier arg partial) McpServer->>McpServer: createCarrierScacCompleter(value) McpServer->>McpServer: "executeGetSupportedShippingLines({search: value})" McpServer-->>Client: "[{MAEU, MSCU, ...}] filtered SCAC codes" Client->>McpServer: tools/call list_containers McpServer->>wrapToolWithContract: handler(args) wrapToolWithContract->>executeListContainers: execute(args, client) executeListContainers-->>wrapToolWithContract: "result {items: [...]}" wrapToolWithContract->>buildListContract: buildContract(result) buildListContract-->>wrapToolWithContract: contract wrapToolWithContract->>buildListResourceLinks: buildResourceLinks(result) buildListResourceLinks-->>wrapToolWithContract: "[resource_link{terminal49://container/{id}}, ...]" wrapToolWithContract->>buildSteeringContent: buildSteeringContent(contract) buildSteeringContent-->>wrapToolWithContract: "{type:text, annotations:{audience:[assistant]}}" wrapToolWithContract-->>McpServer: "{content: [answerBlock, ...resourceLinks, steeringBlock], structuredContent}" McpServer-->>Client: tool resultPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(mcp): register completions capabilit..." | Re-trigger Greptile