Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions chatgpt-app-submission.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,18 @@
"open_world_justification": "Changes only records in the user's own Terminal49 account; does not publish content or change public or third-party systems.",
"destructive_justification": "Setting a field overwrites its previous value, and null clears it. A call that would overwrite or clear a value never changes anything on its first call: it builds a preview of every record that changes, and the user approves it before the change is made, either in a confirmation dialog the client shows (MCP form elicitation) or by approving the preview in the chat so the client can call again with the confirmation token from that preview. A call that only fills empty fields runs at once. The container or shipment is not deleted."
}
},
"update_record": {
"annotations": {
"readOnlyHint": false,
"openWorldHint": false,
"destructiveHint": true
},
"justifications": {
"read_only_justification": "Adds or removes reference numbers on the user's containers, shipments, or tracking requests, and tags on shipments, when the user asks; it changes those records.",
"open_world_justification": "Changes only records in the user's own Terminal49 account; does not publish content or change public or third-party systems.",
"destructive_justification": "Removing a reference number or tag takes it off the record, so searches, filters, and integrations that use it stop finding the record. A call that removes anything never changes anything on its first call: it builds a preview of the lists now and after, and the user approves it before the change is made, either in a confirmation dialog the client shows (MCP form elicitation) or by approving the preview in the chat so the client can call again with the confirmation token from that preview. A call that only adds runs at once and keeps every value already there. The record is not deleted."
}
}
},
"test_cases": [
Expand Down
3 changes: 2 additions & 1 deletion claude-connector-submission.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"set_party_role",
"summarize_containers",
"track_container",
"update_record",
"whoami"
]
},
Expand All @@ -67,7 +68,7 @@
"Inspect routes and transport events",
"Check supported shipping lines",
"Create new tracking requests",
"Set parties and custom field values on containers and shipments",
"Set parties, custom field values, reference numbers, and tags on containers and shipments",
"Research US ocean import markets: importers, products, origin countries, ports, and carriers"
]
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api-docs/in-depth-guides/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Once configured, verify everything works:
<Step title="Ask Claude to list tools">
> "List the tools available in the Terminal49 MCP server."

Claude should respond with a list of 30 tools including `search_container`, `track_container`, the list tools, and the trade intelligence tools.
Claude should respond with a list of 31 tools including `search_container`, `track_container`, the list tools, and the trade intelligence tools.
</Step>
<Step title="Search for a test container">
> "Using the Terminal49 MCP server, search for container TCLU1234567 and summarize its status."
Expand Down
19 changes: 15 additions & 4 deletions docs/mcp/home.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@ The toolset names are:

- `core`: every tool except the document reads, the write tools below, and the trade intelligence tools. `whoami` reports the toolsets a session serves.
- `documents`: `list_documents` and `get_document`.
- `operations`: `set_party_role` and `set_custom_field`. It is in the default toolsets. Send `X-T49-MCP-Toolsets: core,documents,trade` to keep every other tool without them.
- `operations`: `set_party_role`, `set_custom_field` and `update_record`. It is in the default toolsets. Send `X-T49-MCP-Toolsets: core,documents,trade` to keep every other tool without them.
- `tracking_lifecycle`: `stop_shipment_tracking`, `resume_shipment_tracking` and `cancel_tracking_request`. It is not in the default toolsets; a deployment serves it only when `T49_MCP_TOOLSETS` lists it.
- `reference_numbers`: `add_reference_numbers` and `remove_reference_numbers`. It is not in the default toolsets; a deployment serves it only when `T49_MCP_TOOLSETS` lists it.
- `documents_write`: no tools yet.
- `developer`: no tools yet.
- `trade`: the [trade intelligence tools](#trade-intelligence-tools).
Expand Down Expand Up @@ -719,6 +718,18 @@ Returns the records that changed, those that already matched, and any that faile

---


### `update_record`

Add or remove reference numbers, such as PO or job numbers, on a container, shipment, or tracking request, and tags on a shipment. Adding runs at once and keeps everything already there. Removing any value asks you to approve a preview first, because searches, filters, and integrations that use it stop finding the record.

**Parameters**

- `entity_type`: `container`, `shipment`, or `tracking_request`.
- `entity_id`: the record's Terminal49 ID.
- `ref_numbers`: `{ add, remove }`, up to 20 values each.
- `tags`: `{ add, remove }`, shipments only.

### `list_tracking_requests`

List tracking requests with optional filters and pagination.
Expand Down Expand Up @@ -916,11 +927,11 @@ Extracted data and the source email are text that people outside your account wr

### Write tools

Six tools can change your account's data. `track_container` is in the default `core` toolset. The `operations` toolset, which is in the default toolsets, holds `set_party_role` and `set_custom_field`. The `tracking_lifecycle` toolset, which a deployment serves only when `T49_MCP_TOOLSETS` lists it, holds `stop_shipment_tracking`, `resume_shipment_tracking`, and `cancel_tracking_request`. Each write tool has a tier:
Seven tools can change your account's data. `track_container` is in the default `core` toolset. The `operations` toolset, which is in the default toolsets, holds `set_party_role`, `set_custom_field` and `update_record`. The `tracking_lifecycle` toolset, which a deployment serves only when `T49_MCP_TOOLSETS` lists it, holds `stop_shipment_tracking`, `resume_shipment_tracking`, and `cancel_tracking_request`. Each write tool has a tier:

- **Tier A** tools run in one call. Starting or resuming tracking consumes tracking quota and may incur pay-as-you-go charges. These additive tools carry a client interaction hint and explain that consequence; your client controls its permission UI.

- **Tier C** tools need your approval of a preview. They are marked destructive, and they carry `_meta["anthropic/requiresUserInteraction"]`, to request user interaction in clients that support this hint. `stop_shipment_tracking` and `cancel_tracking_request` ask on every call. `set_party_role` and `set_custom_field` change up to 50 records per call and ask only when a record in the call would lose a party or a value; a call that only adds runs at once.
- **Tier C** tools need your approval of a preview. They are marked destructive, and they carry `_meta["anthropic/requiresUserInteraction"]`, to request user interaction in clients that support this hint. `stop_shipment_tracking` and `cancel_tracking_request` ask on every call. `set_party_role` and `set_custom_field` change up to 50 records per call and ask only when a record in the call would lose a party or a value; a call that only adds runs at once. `update_record` works the same way on one record: adding reference numbers or tags runs at once, and removing any asks first.

A call that needs your approval never changes anything the first time. It reads the record, or every record in the batch, and builds a preview of the record now and after the change. For a batch, the preview lists each record that changes and counts the rest, and one approval covers the whole call. How you approve the preview depends on your AI client.

Expand Down
15 changes: 7 additions & 8 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

## 📦 What's Included

### 🛠️ Tools (35 Available)
### 🛠️ Tools (34 Available)

The default toolsets, `core`, `documents`, `operations` and `trade`, serve these 30:
The default toolsets, `core`, `documents`, `operations` and `trade`, serve these 31:

| Tool | Description | Key Features |
|------|-------------|--------------|
Expand All @@ -46,6 +46,7 @@ The default toolsets, `core`, `documents`, `operations` and `trade`, serve these
| **`whoami`** | Identify the credential, its account, the features it can use, and the session's toolsets | Explains empty or unauthorized results |
| **`set_party_role`** | Attach, replace, or detach a party (such as the drayage carrier) on up to 50 containers or shipments | C: asks before a record loses a party; add runs at once |
| **`set_custom_field`** | Set or clear one of the account's custom fields on up to 50 containers or shipments | C: asks before overwriting or clearing a value; filling empty fields runs at once |
| **`update_record`** | Add or remove reference numbers on a container, shipment, or tracking request, and tags on a shipment | C: asks before removing a value; adding runs at once |
| **`search_importers`** | Find US importers by name, by what they import, or both | Trade intelligence, per account |
| **`get_importer_profile`** | One importer's totals, monthly series, ports, origins, carriers, and products | Trade intelligence, per account |
| **`search_commodities`** | Turn a product description or HS prefix into four-digit HS codes | Trade intelligence, per account |
Expand All @@ -54,15 +55,13 @@ The default toolsets, `core`, `documents`, `operations` and `trade`, serve these
| **`get_trade_breakdown`** | Nested totals over one to three dimensions | Trade intelligence, per account |
| **`get_trade_data_coverage`** | Where trade data starts, the latest and last full month | Trade intelligence, per account |

Two opt-in toolsets add five more. A deployment serves `tracking_lifecycle` or `reference_numbers` only when `T49_MCP_TOOLSETS` lists it:
The opt-in `tracking_lifecycle` toolset adds three more. A deployment serves them only when `T49_MCP_TOOLSETS` lists it:

| Tool | Description | Key Features |
|------|-------------|--------------|
| **`stop_shipment_tracking`** | Stop carrier and terminal updates for a shipment and its containers | C: needs a confirmed preview |
| **`resume_shipment_tracking`** | Start tracking a stopped shipment again | A: runs in one call |
| **`cancel_tracking_request`** | Cancel a tracking request that is still pending or awaiting a manifest. It cannot be undone. | C: needs a confirmed preview |
| **`add_reference_numbers`** | Add reference numbers to a container, shipment, or tracking request. Numbers already there stay. | A: runs in one call |
| **`remove_reference_numbers`** | Remove reference numbers from a container, shipment, or tracking request | C: needs a confirmed preview |

### 🎯 Prompts (3 Workflows)

Expand Down Expand Up @@ -430,7 +429,7 @@ Signed URLs, such as the document download link that `get_document` returns, are

### Toolsets

Each tool belongs to one toolset in `src/tools/registry.ts`: `core`, `documents`, `operations`, `tracking_lifecycle`, `reference_numbers`, `documents_write`, `developer` or `trade`. `list_documents` and `get_document` are in `documents`. `set_party_role` and `set_custom_field` are in `operations`. `stop_shipment_tracking`, `resume_shipment_tracking` and `cancel_tracking_request` are in `tracking_lifecycle`, and `add_reference_numbers` and `remove_reference_numbers` are in `reference_numbers`. No deployment serves either until `T49_MCP_TOOLSETS` lists it. The trade intelligence tools are in `trade`. Every other tool today is in `core`. `documents_write` and `developer` have no tools yet. The default toolsets are `core`, `documents`, `operations` and `trade`. `whoami` reports the toolsets a session serves and whether it has write tools.
Each tool belongs to one toolset in `src/tools/registry.ts`: `core`, `documents`, `operations`, `tracking_lifecycle`, `documents_write`, `developer` or `trade`. `list_documents` and `get_document` are in `documents`. `set_party_role`, `set_custom_field` and `update_record` are in `operations`. `stop_shipment_tracking`, `resume_shipment_tracking` and `cancel_tracking_request` are in `tracking_lifecycle`, which no deployment serves until `T49_MCP_TOOLSETS` lists it. The trade intelligence tools are in `trade`. Every other tool today is in `core`. `documents_write` and `developer` have no tools yet. The default toolsets are `core`, `documents`, `operations` and `trade`. `whoami` reports the toolsets a session serves and whether it has write tools.

- `T49_MCP_TOOLSETS` lists the toolsets the server allows. stdio and the HTTP gateway both read it. An unknown name, or a value made only of commas, stops stdio with an error that names the variable. HTTP requests fail with a generic 500, and the gateway logs the cause.
- The `X-T49-MCP-Toolsets` request header can only narrow that list. The server serves the toolsets that appear in both. An unknown name, or a value made only of commas, returns 400.
Expand All @@ -445,7 +444,7 @@ Clients own the permission UI. The server supplies write annotations, consequenc

State checks are best-effort reads, not atomic preconditions on the Terminal49 write. A concurrent edit after the check can still be overwritten, including a field classified as empty before another writer fills it. Preventing that race requires backend conditional writes.

Each write tool has a tier in the registry. A tier A tool, such as `resume_shipment_tracking` or `add_reference_numbers`, is additive or reversible and runs in one call. Its result names the tool that undoes it. A tier C tool, such as `stop_shipment_tracking`, `cancel_tracking_request` or `remove_reference_numbers`, has `destructiveHint: true`, carries `_meta["anthropic/requiresUserInteraction"]: true` to request interaction in clients that support this hint, and needs the user's approval of a preview. Its first call never writes. It reads the record, builds a `preview` with `current`, `proposed` and `reach`, and issues a confirmation token: an HMAC-SHA256, keyed with `T49_MCP_CONFIRMATION_SECRET`, over the channel, the tool name, the other arguments, the account, the credential, an expiry five minutes out, a random nonce and a fingerprint of the record fields the preview showed. Which channel carries the token depends on the request.
Each write tool has a tier in the registry. A tier A tool, such as `resume_shipment_tracking`, is additive or reversible and runs in one call. Its result names the tool that undoes it. A tier C tool, such as `stop_shipment_tracking` or `cancel_tracking_request`, has `destructiveHint: true`, carries `_meta["anthropic/requiresUserInteraction"]: true` to request interaction in clients that support this hint, and needs the user's approval of a preview. Its first call never writes. It reads the record, builds a `preview` with `current`, `proposed` and `reach`, and issues a confirmation token: an HMAC-SHA256, keyed with `T49_MCP_CONFIRMATION_SECRET`, over the channel, the tool name, the other arguments, the account, the credential, an expiry five minutes out, a random nonce and a fingerprint of the record fields the preview showed. Which channel carries the token depends on the request.

**Elicitation channel.** A request whose 2026-07-28 `_meta` envelope declares form elicitation (`io.modelcontextprotocol/clientCapabilities.elicitation.form`, or a bare `elicitation: {}`) gets an `input_required` result per the MCP multi round-trip pattern: one embedded `elicitation/create` request whose message is the preview as text and whose form has one boolean `confirm` field, plus the token as `requestState`. The client shows the user the preview, then retries the call with the user's answer in `inputResponses` and the `requestState` echoed back. The tool verifies the state against the same call, account, credential, expiry and record fingerprint, and:

Expand All @@ -471,7 +470,7 @@ On the elicitation channel the human decides, because only the client can answer

Every tier C tool fingerprints the fields its write changes: `stop_shipment_tracking` the stop time, `cancel_tracking_request` the status, `set_party_role` the party role assignments (by assignment id) in the role on every record, and `set_custom_field` the field's value and its `updated_at` on every record. For the two batch tools a restored record never matches again: a party attached back gets a new assignment id, and a value put back gets a new `updated_at`, so restoring a value or assignment invalidates an earlier approval on a subsequent read. These checks are not atomic across concurrent gateway instances. The shipment resource has no such field, so after `resume_shipment_tracking` a shipment looks as it did before the stop. The server therefore also remembers each confirmation that made a write until it expires, and refuses a second use with `invalid_confirmation` and reason `used`. For single-record lifecycle tools, a definite 4xx refusal releases the confirmation. For batch tools, only a 429 releases it, and only if no write in the call succeeded or may have applied; other record failures require a new preview. A batch 429 stops further work and returns `retry_after_s` plus the per-record results; partial or uncertain writes keep the confirmation spent. After a 5xx or a lost response the write may have happened, so the confirmation stays used and the caller previews again. That memory is per process. On a deployment with several instances, such as the HTTP gateway, another instance can still accept a `stop_shipment_tracking` confirmation once more within its five minutes, after a resume.

The HTTP gateway reads `T49_MCP_CONFIRMATION_SECRET` and has no fallback, because serverless instances share no memory. Without a secret of at least 32 characters, a tier C call that needs the user's approval returns `not_permitted` with a message that names the variable, and the server logs `mcp.config.error`; a call that only adds, such as `set_party_role` with `action: "add"`, still runs. Set it in Vercel before you serve `operations`, `tracking_lifecycle` or `reference_numbers`. stdio uses the variable when it is at least 32 characters. Otherwise it makes a random secret per process, and warns on stderr when the variable is set but too short. Rotating the secret only means an open preview must be requested again.
The HTTP gateway reads `T49_MCP_CONFIRMATION_SECRET` and has no fallback, because serverless instances share no memory. Without a secret of at least 32 characters, a tier C call that needs the user's approval returns `not_permitted` with a message that names the variable, and the server logs `mcp.config.error`; a call that only adds, such as `set_party_role` with `action: "add"`, still runs. Set it in Vercel before you serve `operations` or `tracking_lifecycle`. stdio uses the variable when it is at least 32 characters. Otherwise it makes a random secret per process, and warns on stderr when the variable is set but too short. Rotating the secret only means an open preview must be requested again.

Each call to a write tool, `track_container` included, emits one `mcp.write.audit` event to the log and a Sentry breadcrumb, and to PostHog when the account is known. It carries the tool, tier, phase, outcome, entity type and ID, account ID and credential type, and nothing else. Values, reference numbers, party names, URLs and tokens never reach it.

Expand Down
Loading
Loading