From a9099e28df82df43de34fa8de9b2a36f2ccf2d73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 05:01:44 +0000 Subject: [PATCH] fix(mcp): correct read-only tool annotations Co-authored-by: Akshay Dodeja --- chatgpt-app-submission.json | 36 ++++++++++++------------ packages/mcp/src/annotations.test.ts | 36 +++++++++++++----------- packages/mcp/src/protocol-compat.test.ts | 2 +- packages/mcp/src/server.ts | 20 ++++++------- 4 files changed, 49 insertions(+), 45 deletions(-) diff --git a/chatgpt-app-submission.json b/chatgpt-app-submission.json index e598bc9b..4cbdb6cf 100644 --- a/chatgpt-app-submission.json +++ b/chatgpt-app-submission.json @@ -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." } @@ -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." } diff --git a/packages/mcp/src/annotations.test.ts b/packages/mcp/src/annotations.test.ts index 388fafe5..3fd27abd 100644 --- a/packages/mcp/src/annotations.test.ts +++ b/packages/mcp/src/annotations.test.ts @@ -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', @@ -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(); diff --git a/packages/mcp/src/protocol-compat.test.ts b/packages/mcp/src/protocol-compat.test.ts index f0d9742d..d8f09ae5 100644 --- a/packages/mcp/src/protocol-compat.test.ts +++ b/packages/mcp/src/protocol-compat.test.ts @@ -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, }); diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 03734548..3f7f751d 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -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. @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, }, @@ -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, },