import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { Type, type TSchema } from "@sinclair/typebox"; import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import type { ImageContent, TextContent } from "@earendil-works/pi-ai/compat"; type McpToolResult = { content: Array< | { type: "text"; text: string } | { type: "image"; data: string; mimeType: string } | Record >; isError?: boolean; structuredContent?: Record; }; const endpoint = process.env.SWADS_MCP_URL || "https://ads.mincode.cn/mcp"; const maxRows = Math.max(10, Number(process.env.SWADS_MCP_MAX_ROWS) || 50); const maxTextChars = Math.max( 10_000, Number(process.env.SWADS_MCP_MAX_TEXT_CHARS) || 60_000, ); let clientPromise: Promise | undefined; export function boundSwadsArguments( name: string, args: Record, ) { if (name !== "metrics_semantic_query") return args; const query = args.query; if (!query || typeof query !== "object" || Array.isArray(query)) return args; const requested = Number((query as Record).limit); return { ...args, query: { ...query, limit: Number.isFinite(requested) ? Math.min(Math.max(1, requested), maxRows) : maxRows, }, }; } export function compactSwadsText(text: string) { try { const parsed = JSON.parse(text) as Record; let originalRows: number | undefined; let returnedRows: number | undefined; for (const key of ["rows", "products", "items", "data"]) { const value = parsed[key]; if (!Array.isArray(value)) continue; originalRows = value.length; parsed[key] = value.slice(0, maxRows); returnedRows = (parsed[key] as unknown[]).length; break; } if (originalRows !== undefined) { parsed.response_compaction = { original_rows: originalRows, returned_rows: returnedRows, max_rows: maxRows, note: "Result was bounded for model context safety. Use aggregate queries or narrower filters for additional detail.", }; } const compact = JSON.stringify(parsed); return compact.length <= maxTextChars ? compact : `${compact.slice(0, maxTextChars)}\n[SWADS_RESULT_TRUNCATED_AT_${maxTextChars}_CHARS]`; } catch { return text.length <= maxTextChars ? text : `${text.slice(0, maxTextChars)}\n[SWADS_RESULT_TRUNCATED_AT_${maxTextChars}_CHARS]`; } } async function swadsClient() { const token = process.env.SWADS_MCP_TOKEN?.trim(); if (!token) throw new Error( "SWADS_MCP_NOT_CONFIGURED: set SWADS_MCP_TOKEN in the server environment", ); clientPromise ??= (async () => { const client = new Client({ name: "agent-studio-mini", version: "0.1.0" }); const transport = new StreamableHTTPClientTransport(new URL(endpoint), { requestInit: { headers: { Authorization: `Bearer ${token}` } }, }); await client.connect(transport); return client; })().catch((error) => { clientPromise = undefined; throw error; }); return clientPromise; } function asTool( name: string, description: string, parameters: TSchema, ): ToolDefinition { return defineTool({ name, label: `SW Ads ยท ${name}`, description, parameters, execute: async (_id, args, signal) => { const client = await swadsClient(); const boundedArgs = boundSwadsArguments( name, args as Record, ); const result = (await client.callTool( { name, arguments: boundedArgs }, undefined, { signal }, )) as McpToolResult; const content: Array = []; for (const item of result.content) { if (item.type === "text") content.push({ type: "text", text: compactSwadsText(String(item.text)) }); else if (item.type === "image") content.push({ type: "image", data: String(item.data), mimeType: String(item.mimeType), }); else content.push({ type: "text", text: JSON.stringify(item) }); } if (result.isError) throw new Error( content .filter((item) => item.type === "text") .map((item) => item.text) .join("\n") || "SWADS_MCP_TOOL_FAILED", ); return { content, details: { source: "swads-mcp", contextBounded: true, maxRows, maxTextChars, }, }; }, }); } const optionalAccount = { external_account_id: Type.Optional(Type.Union([Type.String(), Type.Null()])), tenant_id: Type.Optional(Type.String()), }; export function createSwadsReadTools(): ToolDefinition[] { return [ asTool( "swads_whoami", "Read the current SW Ads identity, visible accounts, capabilities, timezone, and currency.", Type.Object({}, { additionalProperties: false }), ), asTool( "metrics_catalog", "Read the supported SW Ads metrics, dimensions, and semantic-query schema before querying data.", Type.Object({}, { additionalProperties: false }), ), asTool( "metrics_semantic_query", "Run a read-only SW Ads semantic metrics query for the current or specified account.", Type.Object( { ...optionalAccount, query: Type.Object({}, { additionalProperties: true }), }, { additionalProperties: false }, ), ), asTool( "commerce_list_products", "Read the TikTok Shop product catalog and current product metadata.", Type.Object( { ...optionalAccount, authorized_business_center_id: Type.Optional( Type.Union([Type.String(), Type.Null()]), ), external_shop_id: Type.Optional(Type.Union([Type.String(), Type.Null()])), only_selectable: Type.Optional(Type.Boolean()), }, { additionalProperties: false }, ), ), ]; }