From 52ecd71b9c68aa5f2e0e5037bb520d5554a400d0 Mon Sep 17 00:00:00 2001 From: Rachel Lee Nabors Date: Wed, 21 Jan 2026 01:03:22 +0000 Subject: [PATCH 1/5] Fix copilot-studio meta key to use underscore Change "copilot-studio" to "copilot_studio" to match the expected directory naming convention. Co-Authored-By: Claude Opus 4.5 --- app/en/get-started/mcp-clients/_meta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/en/get-started/mcp-clients/_meta.tsx b/app/en/get-started/mcp-clients/_meta.tsx index c54cd6ef1..9da3de274 100644 --- a/app/en/get-started/mcp-clients/_meta.tsx +++ b/app/en/get-started/mcp-clients/_meta.tsx @@ -20,7 +20,7 @@ const meta: MetaRecord = { "visual-studio-code": { title: "Visual Studio Code", }, - "copilot-studio": { + copilot_studio: { title: "Microsoft Copilot Studio", }, }; From 8ed091eca7434f64fe3251f426facf15207d4789 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Jan 2026 01:06:55 +0000 Subject: [PATCH 2/5] Fix key mismatch for Copilot Studio in _meta.tsx Changed copilot_studio to copilot-studio to match the key used in mcp-client-grid.tsx. This fixes the card rendering issue where the Copilot Studio card was not displaying in the MCP clients grid. --- app/en/get-started/mcp-clients/_meta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/en/get-started/mcp-clients/_meta.tsx b/app/en/get-started/mcp-clients/_meta.tsx index 9da3de274..c54cd6ef1 100644 --- a/app/en/get-started/mcp-clients/_meta.tsx +++ b/app/en/get-started/mcp-clients/_meta.tsx @@ -20,7 +20,7 @@ const meta: MetaRecord = { "visual-studio-code": { title: "Visual Studio Code", }, - copilot_studio: { + "copilot-studio": { title: "Microsoft Copilot Studio", }, }; From 2ba97e3aa9a0b1094b973b4db4c2bc238a260adc Mon Sep 17 00:00:00 2001 From: Rachel Lee Nabors Date: Wed, 21 Jan 2026 01:22:04 +0000 Subject: [PATCH 3/5] Add meta key validation script and pre-commit check - Add scripts/check-meta-keys.ts to validate that _meta.tsx keys match sibling directories or page files - Add pnpm check-meta script command - Add pre-commit hook to validate staged _meta.tsx files - Skips special keys (*, index, ---), external links (href), and separators - Remove orphaned copilot_studio key (directory doesn't exist) Co-Authored-By: Claude Opus 4.5 --- .husky/pre-commit | 13 + app/en/get-started/mcp-clients/_meta.tsx | 3 - package.json | 3 +- scripts/check-meta-keys.ts | 446 +++++++++++++++++++++++ 4 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 scripts/check-meta-keys.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index de35e84a2..1e192ed30 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -29,6 +29,19 @@ if [ -n "$STAGED_DOCS" ]; then fi fi +# --- Check Meta Keys (when _meta.tsx files are changed) --- +STAGED_META=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '_meta\.tsx$' || true) + +if [ -n "$STAGED_META" ]; then + echo "🔍 Checking _meta.tsx keys..." + + if ! pnpm check-meta --staged-only; then + echo "" + echo "❌ Commit blocked: _meta.tsx keys must match sibling directories or files." + exit 1 + fi +fi + # --- Check Redirects (when markdown pages are deleted or renamed) --- # Detect deleted pages (D status) and renamed pages (R status - old path needs redirect) DELETED_PAGES=$(git diff --cached --name-status | grep -E "^D.*page\.(md|mdx)$" | cut -f2 || true) diff --git a/app/en/get-started/mcp-clients/_meta.tsx b/app/en/get-started/mcp-clients/_meta.tsx index c54cd6ef1..cb9deae1a 100644 --- a/app/en/get-started/mcp-clients/_meta.tsx +++ b/app/en/get-started/mcp-clients/_meta.tsx @@ -20,9 +20,6 @@ const meta: MetaRecord = { "visual-studio-code": { title: "Visual Studio Code", }, - "copilot-studio": { - title: "Microsoft Copilot Studio", - }, }; export default meta; diff --git a/package.json b/package.json index 4718c2862..dc2f529e2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "vale:editorial": "pnpm dlx tsx scripts/vale-editorial.ts", "vale:sync": "vale sync", "check-redirects": "pnpm dlx tsx scripts/check-redirects.ts", - "update-links": "pnpm dlx tsx scripts/update-internal-links.ts" + "update-links": "pnpm dlx tsx scripts/update-internal-links.ts", + "check-meta": "pnpm dlx tsx scripts/check-meta-keys.ts" }, "repository": { "type": "git", diff --git a/scripts/check-meta-keys.ts b/scripts/check-meta-keys.ts new file mode 100644 index 000000000..c71c8a67f --- /dev/null +++ b/scripts/check-meta-keys.ts @@ -0,0 +1,446 @@ +#!/usr/bin/env npx tsx + +/** + * Validate that _meta.tsx keys match existing filesystem entries + * + * Usage: + * pnpm check-meta [--staged-only] + * + * Features: + * - Scans all _meta.tsx files in app/en/ + * - Validates that each key corresponds to a sibling directory or page file + * - Skips special keys like "*", "index", "---", and external links + * - --staged-only: Only check staged _meta.tsx files (for pre-commit hook) + */ + +import { execSync } from "node:child_process"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { dirname } from "node:path"; +import fg from "fast-glob"; + +// Colors for terminal output +const colors = { + red: (s: string) => `\x1b[0;31m${s}\x1b[0m`, + green: (s: string) => `\x1b[0;32m${s}\x1b[0m`, + yellow: (s: string) => `\x1b[1;33m${s}\x1b[0m`, + blue: (s: string) => `\x1b[0;34m${s}\x1b[0m`, + dim: (s: string) => `\x1b[2m${s}\x1b[0m`, +}; + +// Parse command line arguments +const args = process.argv.slice(2); +const stagedOnly = args.includes("--staged-only"); + +// Special keys that don't need filesystem entries +const SPECIAL_KEYS = new Set(["*", "index", "---"]); + +// Nested property names that should never be treated as page keys +const NESTED_PROPERTIES = new Set([ + "theme", + "title", + "display", + "href", + "newWindow", + "breadcrumb", + "toc", + "layout", + "copyPage", + "type", +]); + +// Top-level regex patterns +const META_OBJECT_REGEX = + /(?:const|export\s+const)\s+meta\s*(?::\s*\w+)?\s*=\s*\{/; +const KEY_CHAR_REGEX = /[a-zA-Z0-9_-]/; +const MDX_EXTENSION_REGEX = /\.mdx?$/; + +// Constants +const LOOK_BEHIND_LENGTH = 15; + +type MetaError = { + file: string; + key: string; + message: string; +}; + +type ParserState = { + keys: string[]; + depth: number; + index: number; + inString: boolean; + stringChar: string; + currentKey: string; + collectingKey: boolean; +}; + +/** + * Get list of staged _meta.tsx files + */ +function getStagedMetaFiles(): string[] { + try { + const output = execSync( + "git diff --cached --name-only --diff-filter=ACMR", + { + encoding: "utf-8", + } + ); + return output + .split("\n") + .filter((f) => f.endsWith("_meta.tsx") && f.startsWith("app/")); + } catch { + return []; + } +} + +/** + * Get all _meta.tsx files in app/en/ + */ +function getAllMetaFiles(): string[] { + return fg.sync("app/en/**/_meta.tsx", { onlyFiles: true }); +} + +/** + * Simple key extractor - finds keys at depth 1 of the meta object + */ +function extractMetaKeys(content: string): string[] { + const match = META_OBJECT_REGEX.exec(content); + if (!match) { + return []; + } + + const startIndex = (match.index ?? 0) + match[0].length; + const state: ParserState = { + keys: [], + depth: 1, + index: startIndex, + inString: false, + stringChar: "", + currentKey: "", + collectingKey: false, + }; + + while (state.index < content.length && state.depth > 0) { + parseNextChar(content, state); + } + + return state.keys; +} + +/** + * Parse the next character in the content + */ +function parseNextChar(content: string, state: ParserState): void { + const char = content[state.index]; + const prevChar = state.index > 0 ? content[state.index - 1] : ""; + + if (handleStringBoundary(char, prevChar, state)) { + return; + } + + if (state.inString) { + handleStringContent(char, state); + return; + } + + if (char === "`") { + state.index = skipTemplateLiteral(content, state.index); + return; + } + + if (handleBraceDepth(char, state)) { + return; + } + + handleKeyParsing(content, char, state); + state.index += 1; +} + +/** + * Handle string boundary (quote characters) + */ +function handleStringBoundary( + char: string, + prevChar: string, + state: ParserState +): boolean { + if ((char !== '"' && char !== "'") || prevChar === "\\") { + return false; + } + + if (!state.inString) { + state.inString = true; + state.stringChar = char; + if (state.depth === 1) { + state.collectingKey = true; + state.currentKey = ""; + } + } else if (char === state.stringChar) { + state.inString = false; + state.collectingKey = false; + } + state.index += 1; + return true; +} + +/** + * Handle content inside a string + */ +function handleStringContent(char: string, state: ParserState): void { + if (state.collectingKey) { + state.currentKey += char; + } + state.index += 1; +} + +/** + * Handle brace depth tracking + */ +function handleBraceDepth(char: string, state: ParserState): boolean { + if (char === "{") { + state.depth += 1; + state.index += 1; + return true; + } + + if (char === "}") { + state.depth -= 1; + state.index += 1; + return true; + } + + return false; +} + +/** + * Handle key parsing logic + */ +function handleKeyParsing( + content: string, + char: string, + state: ParserState +): void { + // Start collecting unquoted key + if (state.depth === 1 && KEY_CHAR_REGEX.test(char) && !state.collectingKey) { + const start = Math.max(0, state.index - LOOK_BEHIND_LENGTH); + const lookBehind = content.slice(start, state.index).trim(); + if (isKeyStart(lookBehind)) { + state.collectingKey = true; + state.currentKey = char; + } + return; + } + + // Continue collecting unquoted key + if (state.collectingKey && KEY_CHAR_REGEX.test(char)) { + state.currentKey += char; + return; + } + + // Key ends at colon + if (char === ":" && state.depth === 1 && state.currentKey) { + addKeyIfValid(state.keys, state.currentKey); + state.currentKey = ""; + state.collectingKey = false; + } + + // Reset on comma + if (char === "," && state.depth === 1) { + state.currentKey = ""; + state.collectingKey = false; + } +} + +/** + * Skip over a template literal starting at index i + */ +function skipTemplateLiteral(content: string, startIndex: number): number { + let i = startIndex + 1; + while (i < content.length && content[i] !== "`") { + if (content[i] === "\\" && i + 1 < content.length) { + i += 1; + } + i += 1; + } + return i + 1; +} + +/** + * Check if the lookBehind indicates start of a new key + */ +function isKeyStart(lookBehind: string): boolean { + return ( + lookBehind.endsWith("{") || + lookBehind.endsWith(",") || + lookBehind.endsWith("\n") + ); +} + +/** + * Add key to list if it's valid (not special or nested property) + */ +function addKeyIfValid(keys: string[], key: string): void { + if (!(SPECIAL_KEYS.has(key) || NESTED_PROPERTIES.has(key))) { + keys.push(key); + } +} + +/** + * Check if a key has an href property (making it an external link) + */ +function keyHasHref(content: string, key: string): boolean { + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp( + `["']?${escapedKey}["']?\\s*:\\s*\\{[^}]*href\\s*:`, + "s" + ); + return pattern.test(content); +} + +/** + * Check if a key is a separator (type: "separator" or starts with --) + */ +function keyIsSeparator(content: string, key: string): boolean { + if (key.startsWith("--")) { + return true; + } + + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp( + `["']?${escapedKey}["']?\\s*:\\s*\\{[^}]*type\\s*:\\s*["']separator["']`, + "s" + ); + return pattern.test(content); +} + +/** + * Get valid sibling names for a _meta.tsx file + */ +function getValidSiblings(metaFilePath: string): Set { + const dir = dirname(metaFilePath); + const siblings = new Set(); + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name === "_meta.tsx") { + continue; + } + + if (entry.isDirectory()) { + siblings.add(entry.name); + continue; + } + + const isMarkdown = + entry.name.endsWith(".mdx") || entry.name.endsWith(".md"); + const isPage = entry.name === "page.mdx" || entry.name === "page.md"; + + if (isMarkdown && !isPage) { + siblings.add(entry.name.replace(MDX_EXTENSION_REGEX, "")); + } + } + } catch { + // Directory doesn't exist or can't be read + } + + return siblings; +} + +/** + * Validate a single _meta.tsx file + */ +function validateMetaFile(filePath: string): MetaError[] { + const errors: MetaError[] = []; + + if (!existsSync(filePath)) { + return errors; + } + + const content = readFileSync(filePath, "utf-8"); + const keys = extractMetaKeys(content); + const validSiblings = getValidSiblings(filePath); + + for (const key of keys) { + if (keyHasHref(content, key) || keyIsSeparator(content, key)) { + continue; + } + + if (!validSiblings.has(key)) { + const suggestions = findSimilarSiblings(key, validSiblings); + let message = `Key "${key}" does not match any sibling directory or file`; + if (suggestions.length > 0) { + message += `. Did you mean: ${suggestions.map((s) => `"${s}"`).join(", ")}?`; + } + errors.push({ file: filePath, key, message }); + } + } + + return errors; +} + +/** + * Find siblings with similar names (e.g., copilot-studio vs copilot_studio) + */ +function findSimilarSiblings( + key: string, + validSiblings: Set +): string[] { + const normalizedKey = key.replace(/[-_]/g, ""); + return [...validSiblings].filter((s) => { + const normalizedSibling = s.replace(/[-_]/g, ""); + return normalizedKey === normalizedSibling; + }); +} + +/** + * Main function + */ +function main(): void { + console.log(colors.blue("🔍 Checking _meta.tsx keys...\n")); + + const metaFiles = stagedOnly ? getStagedMetaFiles() : getAllMetaFiles(); + + if (metaFiles.length === 0) { + if (stagedOnly) { + console.log(colors.dim("No staged _meta.tsx files to check")); + } else { + console.log(colors.yellow("No _meta.tsx files found")); + } + process.exit(0); + } + + console.log( + colors.dim(`Checking ${metaFiles.length} _meta.tsx file(s)...\n`) + ); + + const allErrors: MetaError[] = []; + + for (const file of metaFiles) { + const errors = validateMetaFile(file); + allErrors.push(...errors); + } + + if (allErrors.length === 0) { + console.log(colors.green("✅ All _meta.tsx keys are valid\n")); + process.exit(0); + } + + console.log(colors.red(`❌ Found ${allErrors.length} invalid key(s):\n`)); + + for (const error of allErrors) { + console.log(colors.yellow(` ${error.file}:`)); + console.log(` ${error.message}\n`); + } + + console.log( + colors.dim( + "Meta keys must match sibling directories or page files (without extension)." + ) + ); + console.log(colors.dim("Special keys like *, index, and --- are allowed.\n")); + + process.exit(1); +} + +main(); From c188cfff8d22da8644354219a2b80bf432e09338 Mon Sep 17 00:00:00 2001 From: Rachel Lee Nabors Date: Wed, 21 Jan 2026 01:27:16 +0000 Subject: [PATCH 4/5] Move copilot-studio doc to get-started/mcp-clients - Move page from guides/tool-calling/mcp-clients/ to get-started/mcp-clients/ - Add copilot-studio entry to _meta.tsx Co-Authored-By: Claude Opus 4.5 --- app/en/get-started/mcp-clients/_meta.tsx | 3 +++ .../mcp-clients/copilot-studio/page.mdx | 0 2 files changed, 3 insertions(+) rename app/en/{guides/tool-calling => get-started}/mcp-clients/copilot-studio/page.mdx (100%) diff --git a/app/en/get-started/mcp-clients/_meta.tsx b/app/en/get-started/mcp-clients/_meta.tsx index cb9deae1a..c54cd6ef1 100644 --- a/app/en/get-started/mcp-clients/_meta.tsx +++ b/app/en/get-started/mcp-clients/_meta.tsx @@ -20,6 +20,9 @@ const meta: MetaRecord = { "visual-studio-code": { title: "Visual Studio Code", }, + "copilot-studio": { + title: "Microsoft Copilot Studio", + }, }; export default meta; diff --git a/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx b/app/en/get-started/mcp-clients/copilot-studio/page.mdx similarity index 100% rename from app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx rename to app/en/get-started/mcp-clients/copilot-studio/page.mdx From ba0d97eb6603d1c6cd9bad44ba0fadbc193443ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 21 Jan 2026 01:27:57 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A4=96=20Regenerate=20LLMs.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/llms.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/llms.txt b/public/llms.txt index caae727fa..121e7a700 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,4 +1,4 @@ - + # Arcade @@ -239,7 +239,7 @@ Arcade delivers three core capabilities: Deploy agents even your security team w - [Understanding `Context` and tools](https://docs.arcade.dev/en/guides/create-tools/tool-basics/runtime-data-access.md): This documentation page explains the `Context` class used in Arcade tools, detailing how to access runtime capabilities and tool-specific data securely. Users will learn how to utilize the `Context` object to retrieve OAuth tokens, secrets, user information, and to log - [Use Arcade in Claude Desktop](https://docs.arcade.dev/en/get-started/mcp-clients/claude-desktop.md): This documentation page provides a step-by-step guide for users to connect Claude Desktop to an Arcade MCP Gateway, enabling them to utilize custom connectors. It outlines the prerequisites for setup, including creating an Arcade account and obtaining an API key, as well as detailed - [Use Arcade in Cursor](https://docs.arcade.dev/en/get-started/mcp-clients/cursor.md): This documentation page provides a step-by-step guide for connecting Cursor to an Arcade MCP Gateway, enabling users to utilize Arcade tools within the Cursor environment. It outlines the prerequisites for setup, including creating an Arcade account and obtaining an API key, as well as -- [Use Arcade in Microsoft Copilot Studio](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/copilot-studio.md): This documentation page guides users on how to connect Microsoft Copilot Studio to an Arcade MCP Gateway, enabling the integration of Arcade tools within their agents. It outlines the prerequisites, step-by-step instructions for creating or opening an agent, adding an MCP tool, +- [Use Arcade in Microsoft Copilot Studio](https://docs.arcade.dev/en/get-started/mcp-clients/copilot-studio.md): This documentation page guides users on how to connect Microsoft Copilot Studio to an Arcade MCP Gateway, enabling the integration of Arcade tools into their agents. It outlines the prerequisites, step-by-step instructions for creating or opening an agent, adding an MCP tool, - [Use Arcade in Visual Studio Code](https://docs.arcade.dev/en/get-started/mcp-clients/visual-studio-code.md): This documentation page provides a step-by-step guide for connecting Visual Studio Code to an Arcade MCP Gateway, enabling users to set up and run an MCP server within the IDE. It outlines prerequisites, setup instructions, and authentication processes to ensure successful integration. By - [VercelApi](https://docs.arcade.dev/en/resources/integrations/development/vercel-api.md): The VercelApi documentation provides a comprehensive guide for users to manage their Vercel projects, domains, and integrations through various API tools. It outlines available functionalities such as creating and managing access groups, handling deployments, and managing DNS records, enabling - [Walmart Search](https://docs.arcade.dev/en/resources/integrations/search/walmart.md): The Walmart Search documentation provides tools for developers to integrate product search and details retrieval from Walmart into their applications. It outlines how to use the `Walmart.SearchProducts` and `Walmart.GetProductDetails` tools, including parameters for customizing searches and retrieving