Add Beryl Agent and harness source
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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<string, unknown>
|
||||
>;
|
||||
isError?: boolean;
|
||||
structuredContent?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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<Client> | undefined;
|
||||
|
||||
export function boundSwadsArguments(
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
) {
|
||||
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<string, unknown>).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<string, unknown>;
|
||||
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<string, unknown>,
|
||||
);
|
||||
const result = (await client.callTool(
|
||||
{ name, arguments: boundedArgs },
|
||||
undefined,
|
||||
{ signal },
|
||||
)) as McpToolResult;
|
||||
const content: Array<TextContent | ImageContent> = [];
|
||||
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 },
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user