diff --git a/packages/mcp/src/mcp.test.ts b/packages/mcp/src/mcp.test.ts index 56412eb8..116eb703 100644 --- a/packages/mcp/src/mcp.test.ts +++ b/packages/mcp/src/mcp.test.ts @@ -20,15 +20,17 @@ vi.mock('@sentry/node', () => ({ // Stubbed Terminal49Client so server tools can be exercised end-to-end without // hitting the live API. Tests configure these mocks per-case. `vi.hoisted` // is required because vi.mock factories are hoisted above normal declarations. -const { shippingLinesList, containersList } = vi.hoisted(() => ({ +const { shippingLinesList, containersList, shipmentsList } = vi.hoisted(() => ({ shippingLinesList: vi.fn(), containersList: vi.fn(), + shipmentsList: vi.fn(), })); vi.mock('@terminal49/sdk', () => ({ Terminal49Client: class Terminal49Client { shippingLines = { list: shippingLinesList }; containers = { list: containersList }; + shipments = { list: shipmentsList }; }, FeatureNotEnabledError: class FeatureNotEnabledError extends Error {}, NotFoundError: class NotFoundError extends Error {}, @@ -37,6 +39,7 @@ vi.mock('@terminal49/sdk', () => ({ beforeEach(() => { shippingLinesList.mockReset(); containersList.mockReset(); + shipmentsList.mockReset(); }); function _hasResponseContract(schema: unknown): boolean { @@ -300,6 +303,29 @@ class MockTransport { close = vi.fn(); } +async function connectClientForToolCall() { + const handler = createMcpHandler( + () => createTerminal49McpServer('token', 'https://api.test'), + { + legacy: 'stateless', + responseMode: 'json', + }, + ); + const client = new Client( + { name: 'terminal49-tool-output-test', version: '1.0.0' }, + { versionNegotiation: { mode: { pin: '2026-07-28' } } }, + ); + const transport = new StreamableHTTPClientTransport( + new URL('https://mcp.test/mcp'), + { + fetch: (url, init) => handler.fetch(new Request(url, init)), + }, + ); + + await client.connect(transport); + return { client, handler }; +} + describe('MCP server wiring', () => { it('connects without throwing and registers MCP handlers', async () => { const server = createTerminal49McpServer('token', 'https://api.test'); @@ -569,6 +595,92 @@ describe('MCP server wiring', () => { } }); + it.each([ + { + name: 'list_containers', + args: { page: 1, page_size: 10 }, + listMock: containersList, + payload: { + items: [ + { + id: '11111111-1111-1111-1111-111111111111', + number: 'CAIU1234567', + currentStatus: 'available', + terminals: { + podTerminal: { name: 'APM Los Angeles', firmsCode: 'Y123' }, + }, + }, + ], + links: { + self: 'https://api.test/containers?page[number]=1&page[size]=10', + next: 'https://api.test/containers?page[number]=2&page[size]=10', + }, + meta: { total: 42 }, + unsupportedFilters: [], + }, + }, + { + name: 'list_shipments', + args: { + carrier: 'MAEU', + include_containers: true, + page: 1, + page_size: 10, + }, + listMock: shipmentsList, + payload: { + items: [ + { + id: '22222222-2222-2222-2222-222222222222', + billOfLading: 'MAEU123456789', + shippingLineScac: 'MAEU', + containers: [ + { + id: '11111111-1111-1111-1111-111111111111', + number: 'CAIU1234567', + }, + ], + }, + ], + links: { + self: 'https://api.test/shipments?page[number]=1&page[size]=10', + }, + meta: { total: 1 }, + unsupportedFilters: ['carrier'], + }, + }, + ])( + '$name structured content validates with mapped list sidecars', + async ({ name, args, listMock, payload }) => { + listMock.mockResolvedValue(payload); + const { client, handler } = await connectClientForToolCall(); + + try { + const result = await client.callTool({ name, arguments: args }); + + expect(result.structuredContent).toMatchObject({ + ...payload, + _response_contract: { + purpose: expect.any(String), + presentation_guidance: expect.any(String), + suggested_tools: expect.any(Array), + }, + }); + expect( + result.content.some( + (block) => + block.type === 'text' && + block.annotations?.audience?.includes('assistant') && + block.text.includes('_agent_steering'), + ), + ).toBe(true); + } finally { + await client.close(); + await handler.close(); + } + }, + ); + it('marks steering-only content with audience:[assistant] and keeps the answer user-visible', async () => { containersList.mockResolvedValue({ items: [], links: {}, meta: {} }); diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 3f7f751d..d9198443 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -1624,6 +1624,7 @@ export function createTerminal49McpServer( items: z.array(z.record(z.string(), z.any())), links: z.record(z.string(), z.string()).optional(), meta: z.record(z.string(), z.any()).optional(), + unsupportedFilters: z.array(z.string()), _response_contract: responseContractSchema, }), }, @@ -1672,6 +1673,7 @@ export function createTerminal49McpServer( items: z.array(z.record(z.string(), z.any())), links: z.record(z.string(), z.string()).optional(), meta: z.record(z.string(), z.any()).optional(), + unsupportedFilters: z.array(z.string()), _response_contract: responseContractSchema, }), },