From 43d689b64cb66b0cd29abca2ba4bc0e56465e249 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 9 Nov 2026 11:31:57 +1100 Subject: [PATCH] fix(website): normalize agent tools metadata Accept scalar and comma-separated tools declarations alongside YAML lists, and report invalid values with the source filename. Add regression coverage and document the supported formats. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- eng/generate-website-data.mjs | 30 +++++++++++++----- eng/generate-website-data.test.mjs | 51 ++++++++++++++++++++++++++++++ website/README.md | 9 ++++++ 3 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 eng/generate-website-data.test.mjs diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs index 791d697234..bb269ea2c3 100755 --- a/eng/generate-website-data.mjs +++ b/eng/generate-website-data.mjs @@ -185,10 +185,10 @@ function getCurrentCommitSha() { /** * Generate agents metadata */ -function generateAgentsData(gitDates) { +export function generateAgentsData(gitDates, agentsDir = AGENTS_DIR) { const agents = []; const files = fs - .readdirSync(AGENTS_DIR) + .readdirSync(agentsDir) .filter((f) => f.endsWith(".agent.md")); // Track all unique values for filters @@ -196,14 +196,26 @@ function generateAgentsData(gitDates) { const allTools = new Set(); for (const file of files) { - const filePath = path.join(AGENTS_DIR, file); + const filePath = path.join(agentsDir, file); const frontmatter = parseFrontmatter(filePath); const relativePath = path .relative(ROOT_FOLDER, filePath) .replace(/\\/g, "/"); const model = frontmatter?.model || null; - const tools = frontmatter?.tools || []; + const rawTools = frontmatter?.tools; + const tools = + typeof rawTools === "string" + ? rawTools.split(",").map((tool) => tool.trim()).filter(Boolean) + : rawTools ?? []; + if ( + !Array.isArray(tools) || + tools.some((tool) => typeof tool !== "string") + ) { + throw new TypeError( + `Invalid tools in ${relativePath}: expected a string or an array of strings` + ); + } const handoffs = frontmatter?.handoffs || []; // Track unique values @@ -1809,7 +1821,9 @@ async function main() { console.log(`\n✓ All data written to website/public/data/`); } -main().catch((err) => { - console.error("Error generating website data:", err); - process.exit(1); -}); +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + main().catch((err) => { + console.error("Error generating website data:", err); + process.exit(1); + }); +} diff --git a/eng/generate-website-data.test.mjs b/eng/generate-website-data.test.mjs new file mode 100644 index 0000000000..c191e412b3 --- /dev/null +++ b/eng/generate-website-data.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { test } from "node:test"; +import { generateAgentsData } from "./generate-website-data.mjs"; + +function createAgent(t, toolsField) { + const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "website-agents-")); + t.after(() => fs.rmSync(agentsDir, { recursive: true, force: true })); + fs.writeFileSync( + path.join(agentsDir, "sample.agent.md"), + `---\nname: Sample\ndescription: Sample agent\n${toolsField}\n---\n` + ); + return agentsDir; +} + +for (const [name, field, expected] of [ + ["scalar MCP wildcard", "tools: new-relic-mcp-server/*", ["new-relic-mcp-server/*"]], + ["comma-separated tools", "tools: ' read, new-relic-mcp-server/*, edit, '", ["read", "new-relic-mcp-server/*", "edit"]], + ["YAML list", "tools:\n - read\n - new-relic-mcp-server/*", ["read", "new-relic-mcp-server/*"]], + ["omitted tools", "", []], + ["empty list", "tools: []", []], + ["empty string", "tools: ''", []], + ["null tools", "tools:", []], +]) { + test(`generates agent tools and filter facets for ${name}`, (t) => { + const agentsDir = createAgent(t, field); + const data = generateAgentsData(new Map(), agentsDir); + + assert.equal(data.items.length, 1); + assert.deepEqual(data.items[0].tools, expected); + assert.deepEqual(data.filters.tools, [...expected].sort()); + }); +} + +for (const field of [ + "tools: false", + "tools: 42", + "tools: {read: true}", + "tools: [read, 42]", +]) { + test(`reports invalid ${field} with the source filename`, (t) => { + const agentsDir = createAgent(t, field); + + assert.throws(() => generateAgentsData(new Map(), agentsDir), { + name: "TypeError", + message: /Invalid tools in .*sample\.agent\.md: expected a string or an array of strings/, + }); + }); +} diff --git a/website/README.md b/website/README.md index babd6d0b4c..644997db92 100644 --- a/website/README.md +++ b/website/README.md @@ -12,6 +12,15 @@ npm run website:dev # generate data + start the dev server npm run website:build # full production build ``` +Agent metadata generation accepts `tools` as either a YAML string array or a +comma-separated string, including MCP wildcards such as `new-relic-mcp-server/*`. +Both forms produce arrays in the generated metadata and tool filter facets. +Run the focused metadata regression tests from the repository root: + +```bash +node --test eng/generate-website-data.test.mjs +``` + ## Accessibility The website has an automated axe-core + Playwright audit. Run it locally with `npm run website:a11y` from the repository root, or run `npm run a11y` from `website/` after building `dist` first.