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
36 changes: 18 additions & 18 deletions chatgpt-app-submission.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
"tools": {
"search_container": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Searches the user's Terminal49 account without modifying records, but emits redacted operational logs.",
"read_only_justification": "Searches the user's Terminal49 account without modifying records.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
Expand All @@ -34,96 +34,96 @@
},
"get_container": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Retrieves container details without changing the container, but emits redacted operational logs.",
"read_only_justification": "Retrieves container details without changing the container.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"get_shipment_details": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Retrieves shipment details without modifying records, but emits redacted operational logs.",
"read_only_justification": "Retrieves shipment details without modifying records.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"get_container_transport_events": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Retrieves the transport-event timeline without changing it, but emits redacted operational logs.",
"read_only_justification": "Retrieves the transport-event timeline without changing it.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"get_supported_shipping_lines": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Retrieves the supported shipping-line catalog without modifying data, but emits redacted operational logs.",
"read_only_justification": "Retrieves the supported shipping-line catalog without modifying data.",
"open_world_justification": "Reads a closed reference catalog and does not change any external system.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"get_container_route": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Retrieves route and vessel data without modifying it, but emits redacted operational logs.",
"read_only_justification": "Retrieves route and vessel data without modifying it.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"list_shipments": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Lists shipments without modifying them, but emits redacted operational logs.",
"read_only_justification": "Lists shipments without modifying them.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"list_containers": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Lists containers without modifying them, but emits redacted operational logs.",
"read_only_justification": "Lists containers without modifying them.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
},
"list_tracking_requests": {
"annotations": {
"readOnlyHint": false,
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"justifications": {
"read_only_justification": "Lists tracking requests without changing their state, but emits redacted operational logs.",
"read_only_justification": "Lists tracking requests without changing their state.",
"open_world_justification": "Does not publish content or change public or third-party systems.",
"destructive_justification": "Does not delete, overwrite, revoke access, or perform irreversible actions."
}
Expand Down
36 changes: 20 additions & 16 deletions packages/mcp/src/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ function getRegisteredTools(): Record<
}

describe('MCP tool annotations', () => {
// ChatGPT app submission requires readOnlyHint: false for all tools that emit
// operational logs (which all Terminal49 tools do) and openWorldHint: false
// since they only interact with the user's private Terminal49 account.
const allTools = [
const readTools = [
'search_container',
'get_container',
'get_container_route',
Expand All @@ -40,34 +37,41 @@ describe('MCP tool annotations', () => {
'list_containers',
'list_shipments',
'list_tracking_requests',
'track_container',
];
const allTools = [...readTools, 'track_container'];

it('marks all tools with conservative ChatGPT submission annotations', () => {
it('marks fetch-only tools as read-only', () => {
const tools = getRegisteredTools();

for (const name of allTools) {
for (const name of readTools) {
const annotations = tools[name]?.annotations;
expect(annotations, name).toBeDefined();
// All tools emit redacted operational logs, so readOnlyHint is false.
expect(annotations?.readOnlyHint, `${name}.readOnlyHint`).toBe(false);
// All tools interact only with private Terminal49 accounts, not third-party systems.
expect(annotations?.openWorldHint, `${name}.openWorldHint`).toBe(false);
// No tool deletes or overwrites data irreversibly.
expect(annotations?.destructiveHint, `${name}.destructiveHint`).toBe(
false,
);
expect(annotations?.readOnlyHint, `${name}.readOnlyHint`).toBe(true);
}
});

it('marks track_container as non-idempotent', () => {
it('marks track_container as a non-idempotent write', () => {
const tools = getRegisteredTools();
const annotations = tools.track_container?.annotations;

expect(annotations).toBeDefined();
expect(annotations?.readOnlyHint).toBe(false);
expect(annotations?.idempotentHint).toBe(false);
});

it('marks every tool as private-account-only and non-destructive', () => {
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,
);
}
});

it('annotates every registered tool', () => {
const tools = getRegisteredTools();

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/src/protocol-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('MCP protocol compatibility', () => {

for (const tool of tools) {
expect(tool.annotations).toMatchObject({
readOnlyHint: false,
readOnlyHint: tool.name !== 'track_container',
destructiveHint: false,
openWorldHint: false,
});
Expand Down
20 changes: 10 additions & 10 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const TERMINAL49_SERVER_INSTRUCTIONS = `Terminal49 tracks ocean container

Domain vocabulary: SCAC = 4-letter carrier code; BOL = bill of lading and booking number identify a shipment; POL/POD = port of lading/discharge; LFD = last free day (pickup deadline before demurrage accrues); demurrage/detention = late fees; holds = customs/freight/terminal blocks preventing pickup; transport events = carrier milestones (vessel loaded, departed, arrived, discharged, rail, delivered).

Only track_container changes Terminal49 account records: it creates a tracking request to begin monitoring a number. The other tools fetch data, but every tool emits redacted operational logs and therefore declares readOnlyHint false under the submission rubric.
Only track_container changes Terminal49 account records: it creates a tracking request to begin monitoring a number and is marked non-read-only. The other tools only fetch data and are marked read-only. All tools operate within the user's private Terminal49 account and none delete or overwrite data.

Canonical chaining: start with search_container to resolve a container number / BOL / reference into Terminal49 UUIDs, then get_container or get_shipment_details for a snapshot, then get_container_transport_events for the milestone timeline (and get_container_route for multi-leg routing if the account has it). Use get_supported_shipping_lines to resolve a carrier name to its SCAC before track_container. Use list_containers / list_shipments / list_tracking_requests for fleet-level worklists.

Expand Down Expand Up @@ -1209,7 +1209,7 @@ export function createTerminal49McpServer(
'This is the fastest way to find container information. ' +
'Examples: CAIU2885402, MAEU123456789, or any reference number.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1348,7 +1348,7 @@ export function createTerminal49McpServer(
'plus optional related data. Choose includes based on user question and container state. ' +
'Response includes metadata hints to guide follow-up queries.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1391,7 +1391,7 @@ export function createTerminal49McpServer(
'Use this when user asks about a shipment (vs a specific container). ' +
'Returns: Bill of Lading, shipping line, port details, vessel info, ETAs, container list.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1433,7 +1433,7 @@ export function createTerminal49McpServer(
'Use this for questions about journey history, "what happened", timeline analysis, rail tracking. ' +
'More efficient than get_container with transport_events when you only need event data.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1466,7 +1466,7 @@ export function createTerminal49McpServer(
'Returns SCAC codes, full names, and common abbreviations. ' +
'Use this when user asks which carriers are supported or to validate a carrier name.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1514,7 +1514,7 @@ export function createTerminal49McpServer(
'NOTE: This is a paid feature and may not be available for all accounts. ' +
'Use for questions about routing, transshipments, or detailed vessel itinerary.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1598,7 +1598,7 @@ export function createTerminal49McpServer(
'List shipments with optional filters and pagination. ' +
'Use for queries like "show recent shipments" or "shipments for a carrier".',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1646,7 +1646,7 @@ export function createTerminal49McpServer(
'List containers with optional filters and pagination. ' +
'Use for queries like "containers at port" or "latest updates".',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down Expand Up @@ -1698,7 +1698,7 @@ export function createTerminal49McpServer(
'List tracking requests with optional filters and pagination. ' +
'Useful for monitoring recent tracking activity.',
annotations: {
readOnlyHint: false,
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
Expand Down
Loading