Skip to content
Open
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
24 changes: 24 additions & 0 deletions chatgpt-app-submission.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,30 @@
"open_world_justification": "Reads the connected Terminal49 account only and does not change any external system.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"set_party_role": {
"annotations": {
"readOnlyHint": false,
"openWorldHint": false,
"destructiveHint": true
},
"justifications": {
"read_only_justification": "Attaches, replaces, or detaches a party, such as the drayage carrier, on the user's own containers or 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": "Replacing or detaching a party removes that party's assignment from the record. The party, container, and shipment are not deleted, and calling the tool again can restore the assignment."
}
},
"set_custom_field": {
"annotations": {
"readOnlyHint": false,
"openWorldHint": false,
"destructiveHint": true
},
"justifications": {
"read_only_justification": "Sets or clears one of the account's own custom fields on the user's containers or 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": "Setting a field overwrites its previous value, and null clears it. The container or shipment is not deleted, and calling the tool again with the old value restores it."
}
}
},
"test_cases": [
Expand Down
2 changes: 2 additions & 0 deletions claude-connector-submission.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"list_tracking_requests",
"search_container",
"search_docs",
"set_custom_field",
"set_party_role",
"summarize_containers",
"track_container",
"whoami"
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 @@ -170,7 +170,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 14 tools including `search_container`, `track_container`, and list tools.
Claude should respond with a list of 16 tools including `search_container`, `track_container`, and list tools.
</Step>
<Step title="Search for a test container">
> "Using the Terminal49 MCP server, search for container TCLU1234567 and summarize its status."
Expand Down
40 changes: 39 additions & 1 deletion docs/mcp/home.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Input and output recording is disabled by default. Leave `SENTRY_MCP_RECORD_INPU
## Tools reference

<Note>
Every tool is read-only except `track_container`, which creates a tracking request. If your account already has an in-progress tracking request for the number, or a container with that number that is still being tracked, `track_container` returns it instead of creating a new request. A container whose shipment has stopped tracking does not count. A request is in progress while its status is `pending` or `awaiting_manifest`. When you pass `scac`, only an in-progress request under that SCAC counts. Failed or cancelled requests do not block a new one.
Every tool is read-only except three. `set_party_role` and `set_custom_field` change parties and custom fields on your containers and shipments; repeating a call changes nothing more. Because they can detach a party or overwrite a field value, they are marked as destructive for MCP clients. `track_container` creates a tracking request. If your account already has an in-progress tracking request for the number, or a container with that number that is still being tracked, `track_container` returns it instead of creating a new request. A container whose shipment has stopped tracking does not count. A request is in progress while its status is `pending` or `awaiting_manifest`. When you pass `scac`, only an in-progress request under that SCAC counts. Failed or cancelled requests do not block a new one.
</Note>

### `search_container`
Expand Down Expand Up @@ -632,6 +632,44 @@ Find the companies on the account's shipments (customers, shippers, consignees,

---

### `set_party_role`

Attach a party to containers or shipments in a role, replace the party in that role, or detach it. Find the party ID with `list_parties` first.

**Parameters**

- `container_ids` or `shipment_ids` *(array of IDs)* - 1 to 50 records; pass one, not both.
- `role` *(string)* - containers support `pickup_dray_carrier`; shipments also support `shipper`, `consignee`, `notify_party`, `customs_broker`, `customer`, and `freight_forwarder`.
- `party_id` *(string)* - the party's ID.
- `action` *(string)* - `add`, `replace`, or `remove`. `add` attaches the party beside any others in the role. A record holds one `pickup_dray_carrier` and one `customer`, so `add` fails on a record where a different party already holds one of those; use `replace` to swap it. `replace` makes the party the only one in the role and detaches the rest. It does not replace a shipment's `customer`: remove the current customer, then add the new one. `remove` detaches the party.

Returns the records that changed, those that already matched, and any that failed. A call stops starting new records after about 20 seconds; any it did not reach are listed in `not_attempted`, unchanged, so you can call again with them.

**Good for**

- "Set the drayage carrier to Manzo Transport on these containers." (`replace`)

---

### `set_custom_field`

Set or clear one of your account's custom fields on containers or shipments.

**Parameters**

- `entity` *(string)* - `container` or `shipment`.
- `ids` *(array of IDs)* - 1 to 50 records.
- `api_slug` *(string)* - the custom field's API slug.
- `value` *(string, number, boolean, array of strings, or null)* - the new value; pass an array of option values for a multi-select field, and `null` to clear the field.

Returns the records that changed, those that already matched, and any that failed. A call stops starting new records after about 20 seconds; any it did not reach are listed in `not_attempted`, unchanged, so you can call again with them.

**Good for**

- "Set Sales Rep to Sofia Cortina on these containers."

---

### `list_tracking_requests`

List tracking requests with optional filters and pagination.
Expand Down
41 changes: 40 additions & 1 deletion packages/mcp/TOOLS_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,45 @@ whoami()

---

### 15. `set_party_role`
**Purpose**: Attach a party to containers or shipments in a role, replace the party in that role, or detach it (destructive writes)

**Usage**:
```typescript
set_party_role({
container_ids: ["<container-id>"], // or shipment_ids; 1-50, not both
role: "pickup_dray_carrier", // containers support only this role
party_id: "<party-id-from-list_parties>",
action: "replace" // or "add" (beside others) or "remove"
})
```

**Returns**:
- `updated`, `unchanged` (already matched), `failed` (`id`, `error`), and `not_attempted` (the call ran out of time before reaching them; nothing changed on them)

**When to Use**: Only when the user asks to change these records. Resolve the company with `list_parties` first. `pickup_dray_carrier` and `customer` hold one party per record: `add` fails where a different party holds one, and `replace` swaps it (except a shipment `customer`, which must be removed, then added).

---

### 16. `set_custom_field`
**Purpose**: Set or clear one of the account's custom fields on containers or shipments (destructive writes: overwrites or clears the previous value)

**Usage**:
```typescript
set_custom_field({
entity: "container", // or "shipment"
ids: ["<container-id>"], // 1-50
api_slug: "sales_rep",
value: "Sofia Cortina" // an array of option values for a multi-select field; null clears
})
```

**Returns**: the same `updated`, `unchanged`, `failed`, and `not_attempted` lists as `set_party_role`

**When to Use**: Only when the user asks to change these records. Read the field's `api_slug` from `get_container` or `get_shipment_details` with custom fields first.

---

## MCP Resources

### 1. `terminal49://container/{id}`
Expand Down Expand Up @@ -590,7 +629,7 @@ get_container_transport_events({ id: "abc-123" })

## Summary

With these 14 tools and 4 resources, the LLM can:
With these 16 tools and 4 resources, the LLM can:

✅ Find any container or shipment
✅ Get fast status updates
Expand Down
13 changes: 9 additions & 4 deletions packages/mcp/scripts/http-protocol-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,18 @@ try {
}

const { tools } = await client.listTools();
const requiredTools = [
'search_docs',
'whoami',
'set_party_role',
'set_custom_field',
];
if (
tools.length !== 14 ||
!tools.some((tool) => tool.name === 'search_docs') ||
!tools.some((tool) => tool.name === 'whoami')
tools.length !== 16 ||
!requiredTools.every((name) => tools.some((tool) => tool.name === name))
) {
throw new Error(
`Expected 14 preview tools including search_docs and whoami over ${protocolVersion}, received ${tools.length}`,
`Expected 16 preview tools including ${requiredTools.join(', ')} over ${protocolVersion}, received ${tools.length}`,
);
}

Expand Down
13 changes: 9 additions & 4 deletions packages/mcp/scripts/protocol-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,18 @@ try {
}

const { tools } = await client.listTools();
const requiredTools = [
'search_docs',
'whoami',
'set_party_role',
'set_custom_field',
];
if (
tools.length !== 14 ||
!tools.some((tool) => tool.name === 'search_docs') ||
!tools.some((tool) => tool.name === 'whoami')
tools.length !== 16 ||
!requiredTools.every((name) => tools.some((tool) => tool.name === name))
) {
throw new Error(
`Expected 14 tools including search_docs and whoami over ${protocolVersion}, received ${tools.length}`,
`Expected 16 tools including ${requiredTools.join(', ')} over ${protocolVersion}, received ${tools.length}`,
);
}

Expand Down
31 changes: 22 additions & 9 deletions packages/mcp/src/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ describe('MCP tool annotations', () => {
'list_tracking_requests',
'whoami',
];
const allTools = [...readTools, 'track_container'];
const writeTools = ['set_custom_field', 'set_party_role', 'track_container'];
const allTools = [...readTools, ...writeTools];

it('marks fetch-only tools as read-only', () => {
const tools = getRegisteredTools();
Expand Down Expand Up @@ -128,29 +129,41 @@ describe('MCP tool annotations', () => {
expect(annotations?.idempotentHint).toBe(false);
});

it('advertises track_container as the only write and no destructive tools', () => {
it('marks the record edit tools as idempotent writes', () => {
const tools = getRegisteredTools();
const writeTools = Object.entries(tools)

for (const name of ['set_party_role', 'set_custom_field']) {
expect(tools[name]?.annotations?.readOnlyHint, name).toBe(false);
expect(tools[name]?.annotations?.idempotentHint, name).toBe(true);
}
});

it('advertises only the expected writes, and the record edits as destructive', () => {
// MCP: destructiveHint false means the tool only adds. set_party_role
// detaches parties (remove, replace) and set_custom_field overwrites or
// clears values, so both must say true. track_container only creates.
const tools = getRegisteredTools();
const writes = Object.entries(tools)
.filter(([, tool]) => tool.annotations?.readOnlyHint === false)
.map(([name]) => name);
const destructiveTools = Object.entries(tools)
.filter(([, tool]) => tool.annotations?.destructiveHint === true)
.map(([name]) => name);

expect(writeTools).toEqual(['track_container']);
expect(destructiveTools).toEqual([]);
expect(writes.sort()).toEqual(writeTools);
expect(destructiveTools.sort()).toEqual([
'set_custom_field',
'set_party_role',
]);
});

it('marks every tool as private-account-only and non-destructive', () => {
it('marks every tool as private-account-only', () => {
const tools = getRegisteredTools();

for (const name of allTools) {
const annotations = tools[name]?.annotations;
expect(annotations, name).toBeDefined();
expect(annotations?.openWorldHint, `${name}.openWorldHint`).toBe(false);
expect(annotations?.destructiveHint, `${name}.destructiveHint`).toBe(
false,
);
}
});

Expand Down
16 changes: 15 additions & 1 deletion packages/mcp/src/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ describe('MCP server wiring', () => {
);
const prompts = Object.keys((server as any)._registeredPrompts || {});

expect(tools).toHaveLength(14);
expect(tools).toHaveLength(16);
expect(tools).toContain('search_container');
expect(tools).toContain('track_container');
expect(tools).toContain('get_container');
Expand All @@ -371,6 +371,8 @@ describe('MCP server wiring', () => {
expect(tools).toContain('list_tracking_requests');
expect(tools).toContain('search_docs');
expect(tools).toContain('whoami');
expect(tools).toContain('set_party_role');
expect(tools).toContain('set_custom_field');

expect(prompts).toHaveLength(3);
expect(prompts).toContain('track-shipment');
Expand Down Expand Up @@ -404,6 +406,18 @@ describe('MCP server wiring', () => {
list_containers: {},
summarize_containers: { group_by: 'pod_terminal' },
list_parties: { search: 'Acme' },
set_party_role: {
container_ids: [id],
role: 'pickup_dray_carrier',
party_id: id,
action: 'add',
},
set_custom_field: {
entity: 'container',
ids: [id],
api_slug: 'sales_rep',
value: 'X',
},
list_tracking_requests: {},
search_docs: { query: 'webhooks' },
whoami: {},
Expand Down
14 changes: 10 additions & 4 deletions packages/mcp/src/protocol-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('MCP protocol compatibility', () => {
]),
);

expect(tools).toHaveLength(14);
expect(tools).toHaveLength(16);
for (const tool of tools) {
expect(tool.name.length, tool.name).toBeLessThanOrEqual(64);
expect(tool.inputSchema.properties, tool.name).not.toHaveProperty(
Expand Down Expand Up @@ -306,15 +306,21 @@ describe('MCP protocol compatibility', () => {
client.listResourceTemplates(),
]);

expect(tools).toHaveLength(14);
expect(tools).toHaveLength(16);
expect(prompts).toHaveLength(3);
expect(resources).toHaveLength(3);
expect(resourceTemplates).toHaveLength(1);

for (const tool of tools) {
expect(tool.annotations).toMatchObject({
readOnlyHint: tool.name !== 'track_container',
destructiveHint: false,
readOnlyHint: ![
'track_container',
'set_party_role',
'set_custom_field',
].includes(tool.name),
destructiveHint: ['set_party_role', 'set_custom_field'].includes(
tool.name,
),
openWorldHint: tool.name === 'search_docs',
});
}
Expand Down
Loading
Loading