From 58bfa7d57001d89851676c0a730e7298cca2852c Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 20:54:40 -0300 Subject: [PATCH 01/11] fix: resolve P1-P3 findings from tool-metadata-ui-and-markdown review - P1: create metadata-report script and tool-metadata-audit utility; add pnpm dlx tsx invocation to package.json so pnpm metadata-report works - P2: resolve data dir relative to script file (cwd-independent); exit 1 on error instead of silently swallowing it - P2: gate page-size and sort dropdowns behind enableFilters; export filterTools with operations/behavior-flag filtering via options object; extract helpers to stay within complexity and param-count lint rules; add 13 tests covering the new filter paths - P2: export getSharedServiceDomain from toolkit-page (was internal); add 5 tests covering shared/mixed/empty/multi-domain edge cases - P3: fix telemetry page title typo (Telemtry -> Telemetry) Made-with: Cursor --- .../components/available-tools-table.tsx | 228 +++++++++++------- .../toolkit-docs/components/toolkit-page.tsx | 68 +++++- app/en/references/mcp/telemetry/page.mdx | 2 +- package.json | 3 +- .../scripts/report-tool-metadata.ts | 37 +++ .../src/utils/tool-metadata-audit.ts | 154 ++++++++++++ .../available-tools-filter-behavior.test.ts | 61 +++++ .../available-tools-filter-operations.test.ts | 61 +++++ .../app-lib/shared-service-domain.test.ts | 44 ++++ 9 files changed, 566 insertions(+), 92 deletions(-) create mode 100644 toolkit-docs-generator/scripts/report-tool-metadata.ts create mode 100644 toolkit-docs-generator/src/utils/tool-metadata-audit.ts create mode 100644 toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts create mode 100644 toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts create mode 100644 toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts diff --git a/app/_components/toolkit-docs/components/available-tools-table.tsx b/app/_components/toolkit-docs/components/available-tools-table.tsx index b0f9af07e..16abcc9d7 100644 --- a/app/_components/toolkit-docs/components/available-tools-table.tsx +++ b/app/_components/toolkit-docs/components/available-tools-table.tsx @@ -22,7 +22,11 @@ import { import { useEffect, useMemo, useRef, useState } from "react"; import { SCROLLING_CELL } from "../constants"; -import type { AvailableToolsTableProps, SecretType } from "../types"; +import type { + AvailableToolsTableProps, + BehaviorFlagKey, + SecretType, +} from "../types"; import { normalizeScopes } from "./scopes-display"; const DEFAULT_PAGE_SIZE = 25; @@ -487,40 +491,82 @@ export function sortTools( } } -function filterTools( +export type FilterToolsOptions = { + activeOperations?: Set; + behaviorFlags?: Partial>; +}; + +function matchesFilterCategory( + tool: AvailableToolsTableProps["tools"][number], + filter: AvailableToolsFilter +): boolean { + const hasScopes = buildScopeDisplayItems(tool.scopes ?? []).length > 0; + const hasSecrets = + (tool.secretsInfo?.length ?? 0) > 0 || (tool.secrets?.length ?? 0) > 0; + + switch (filter) { + case "has_scopes": + return hasScopes; + case "no_scopes": + return !hasScopes; + case "has_secrets": + return hasSecrets; + case "no_secrets": + return !hasSecrets; + default: + return true; + } +} + +function matchesOperations( + tool: AvailableToolsTableProps["tools"][number], + activeOperations: Set +): boolean { + if (activeOperations.size === 0) { + return true; + } + const toolOps = tool.metadata?.behavior?.operations ?? []; + return toolOps.some((op) => activeOperations.has(op)); +} + +function matchesBehaviorFlags( + tool: AvailableToolsTableProps["tools"][number], + behaviorFlags: Partial> +): boolean { + for (const [key, expected] of Object.entries(behaviorFlags) as [ + BehaviorFlagKey, + boolean, + ][]) { + if (tool.metadata?.behavior?.[key] !== expected) { + return false; + } + } + return true; +} + +export function filterTools( tools: AvailableToolsTableProps["tools"], query: string, - filter: AvailableToolsFilter + filter: AvailableToolsFilter, + options: FilterToolsOptions = {} ): AvailableToolsTableProps["tools"] { + const { activeOperations = new Set(), behaviorFlags = {} } = options; const normalizedQuery = query.trim().toLowerCase(); return tools.filter((tool) => { const haystack = [tool.name, tool.qualifiedName, tool.description ?? ""] .join(" ") .toLowerCase(); - const matchesQuery = - normalizedQuery.length === 0 || haystack.includes(normalizedQuery); - - if (!matchesQuery) { + if (normalizedQuery.length > 0 && !haystack.includes(normalizedQuery)) { return false; } - - const hasScopes = buildScopeDisplayItems(tool.scopes ?? []).length > 0; - const hasSecrets = - (tool.secretsInfo?.length ?? 0) > 0 || (tool.secrets?.length ?? 0) > 0; - - switch (filter) { - case "has_scopes": - return hasScopes; - case "no_scopes": - return !hasScopes; - case "has_secrets": - return hasSecrets; - case "no_secrets": - return !hasSecrets; - default: - return true; + if (!matchesFilterCategory(tool, filter)) { + return false; } + if (!matchesOperations(tool, activeOperations)) { + return false; + } + return matchesBehaviorFlags(tool, behaviorFlags); }); } @@ -611,73 +657,79 @@ export function AvailableToolsTable({ )} {enableFilters && ( - { + setFilter(value as AvailableToolsFilter); + setPage(1); + }} + value={filter} + > + + + + + All tools + + Requires secrets only + + + No secrets required + + + + + + + + + {PAGE_SIZE_OPTIONS.map((size) => ( + + {size} / page + + ))} + + + + )} - - {filteredTools.length} diff --git a/app/_components/toolkit-docs/components/toolkit-page.tsx b/app/_components/toolkit-docs/components/toolkit-page.tsx index a9e969019..f8ef0726e 100644 --- a/app/_components/toolkit-docs/components/toolkit-page.tsx +++ b/app/_components/toolkit-docs/components/toolkit-page.tsx @@ -1,13 +1,17 @@ "use client"; -import { Button } from "@arcadeai/design-system"; +import { Badge, Button } from "@arcadeai/design-system"; import { ArrowDown, ArrowUp, KeyRound } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import ScopePicker from "../../scope-picker"; import ToolFooter from "../../tool-footer"; -import { getPackageName } from "../constants"; +import { + getPackageName, + TOOL_METADATA_FALLBACK_STYLE, + TOOL_METADATA_SERVICE_DOMAIN_STYLES, +} from "../constants"; // Scroll detection thresholds const SCROLL_SHOW_BUTTONS_THRESHOLD = 300; @@ -206,6 +210,45 @@ function inferToolkitType(toolkitId: string, type: ToolkitType): ToolkitType { return type; } +export function getSharedServiceDomain( + tools: ReadonlyArray<{ + metadata?: { + classification?: { + serviceDomains?: string[]; + } | null; + } | null; + }> +): string | null { + if (tools.length === 0) { + return null; + } + + let sharedDomain: string | null = null; + + for (const tool of tools) { + const serviceDomains = tool.metadata?.classification?.serviceDomains; + if (!serviceDomains || serviceDomains.length !== 1) { + return null; + } + + const domain = serviceDomains[0]; + if (!domain) { + return null; + } + + if (sharedDomain === null) { + sharedDomain = domain; + continue; + } + + if (sharedDomain !== domain) { + return null; + } + } + + return sharedDomain; +} + function toTitleCaseCategory(category: ToolkitCategory): string { return category .split("-") @@ -557,6 +600,10 @@ export function ToolkitPage({ data }: ToolkitPageProps) { }), [data.id, data.metadata] ); + const sharedServiceDomain = useMemo( + () => getSharedServiceDomain(tools), + [tools] + ); const handleScopeSelectionChange = (toolNames: string[]) => { setSelectedTools(new Set(toolNames)); @@ -583,6 +630,22 @@ export function ToolkitPage({ data }: ToolkitPageProps) {

{data.label}

+ {sharedServiceDomain && ( +
+ + Service domain + + + {sharedServiceDomain.replace(/_/g, " ").toUpperCase()} + +
+ )} diff --git a/app/en/references/mcp/telemetry/page.mdx b/app/en/references/mcp/telemetry/page.mdx index d4f0119a4..b8c8db3ec 100644 --- a/app/en/references/mcp/telemetry/page.mdx +++ b/app/en/references/mcp/telemetry/page.mdx @@ -1,5 +1,5 @@ --- -title: "Arcade MCP Telemtry" +title: "Arcade MCP Telemetry" description: "Learn about what data we track when using arcade-mcp" --- diff --git a/package.json b/package.json index f796fcd9b..8528807f0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "vale:sync": "vale sync", "check-redirects": "pnpm dlx tsx scripts/check-redirects.ts", "update-links": "pnpm dlx tsx scripts/update-internal-links.ts", - "check-meta": "pnpm dlx tsx scripts/check-meta-keys.ts" + "check-meta": "pnpm dlx tsx scripts/check-meta-keys.ts", + "metadata-report": "pnpm dlx tsx toolkit-docs-generator/scripts/report-tool-metadata.ts" }, "repository": { "type": "git", diff --git a/toolkit-docs-generator/scripts/report-tool-metadata.ts b/toolkit-docs-generator/scripts/report-tool-metadata.ts new file mode 100644 index 000000000..51624b8e0 --- /dev/null +++ b/toolkit-docs-generator/scripts/report-tool-metadata.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * CLI script to report tool metadata coverage and distinct enum values. + * Resolves data directory relative to this script, so it works regardless of cwd. + */ + +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { collectToolMetadataStats } from "../src/utils/tool-metadata-audit.ts"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DATA_DIR = join(__dirname, "..", "data", "toolkits"); + +async function main(): Promise { + const stats = await collectToolMetadataStats({ dataDir: DATA_DIR }); + + console.log("Tool metadata report"); + console.log("==================="); + console.log(`Total tools: ${stats.coverage.totalTools}`); + console.log(`With metadata: ${stats.coverage.withMetadata}`); + console.log( + `Distinct operations: ${stats.distinct.operations.join(", ") || "none"}` + ); + console.log( + `Distinct service domains: ${stats.distinct.serviceDomains.join(", ") || "none"}` + ); + console.log(`Tools with extras: ${stats.extras.toolsWithExtras}`); + if (stats.extras.distinctKeys.length > 0) { + console.log(`Extras keys: ${stats.extras.distinctKeys.join(", ")}`); + } +} + +main().catch((err) => { + console.error("metadata-report failed:", err); + process.exit(1); +}); diff --git a/toolkit-docs-generator/src/utils/tool-metadata-audit.ts b/toolkit-docs-generator/src/utils/tool-metadata-audit.ts new file mode 100644 index 000000000..b3e80cbd0 --- /dev/null +++ b/toolkit-docs-generator/src/utils/tool-metadata-audit.ts @@ -0,0 +1,154 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +type BooleanCounts = { true: number; false: number; undefined: number }; +type BooleanKey = "readOnly" | "destructive" | "idempotent" | "openWorld"; + +type ToolMeta = { + classification?: { serviceDomains?: string[] }; + behavior?: { + operations?: string[]; + readOnly?: boolean; + destructive?: boolean; + idempotent?: boolean; + openWorld?: boolean; + }; + extras?: Record | null; +} | null; + +type ToolEntry = { metadata?: ToolMeta }; + +export type ToolMetadataStats = { + coverage: { + totalTools: number; + withMetadata: number; + }; + distinct: { + operations: string[]; + serviceDomains: string[]; + }; + booleans: { + readOnly: BooleanCounts; + destructive: BooleanCounts; + idempotent: BooleanCounts; + openWorld: BooleanCounts; + }; + extras: { + toolsWithExtras: number; + distinctKeys: string[]; + }; +}; + +type AccumulatorState = { + operationsSet: Set; + serviceDomainsSet: Set; + extrasKeysSet: Set; + totalTools: number; + withMetadata: number; + toolsWithExtras: number; + booleans: Record; +}; + +function incrementBoolean(counts: BooleanCounts, val: boolean | undefined) { + if (val === true) counts.true++; + else if (val === false) counts.false++; + else counts.undefined++; +} + +function accumulateToolMeta( + meta: NonNullable, + acc: AccumulatorState +) { + for (const op of meta.behavior?.operations ?? []) { + acc.operationsSet.add(op); + } + for (const sd of meta.classification?.serviceDomains ?? []) { + acc.serviceDomainsSet.add(sd); + } + + const b = meta.behavior; + if (b) { + for (const key of [ + "readOnly", + "destructive", + "idempotent", + "openWorld", + ] as const) { + incrementBoolean(acc.booleans[key], b[key]); + } + } + + if (meta.extras && Object.keys(meta.extras).length > 0) { + acc.toolsWithExtras++; + for (const k of Object.keys(meta.extras)) { + acc.extrasKeysSet.add(k); + } + } +} + +function accumulateTools(tools: ToolEntry[], acc: AccumulatorState) { + for (const tool of tools) { + acc.totalTools++; + const meta = tool.metadata; + if (!meta) continue; + acc.withMetadata++; + accumulateToolMeta(meta, acc); + } +} + +async function parseJsonFile( + path: string +): Promise<{ tools?: unknown[] } | null> { + try { + const content = await readFile(path, "utf-8"); + return JSON.parse(content) as { tools?: unknown[] }; + } catch (err) { + console.warn( + `[tool-metadata-audit] Skipping unreadable or malformed JSON: ${path} — ${err}` + ); + return null; + } +} + +export async function collectToolMetadataStats(opts: { + dataDir: string; +}): Promise { + const { dataDir } = opts; + const files = await readdir(dataDir); + const jsonFiles = files.filter((f) => f.endsWith(".json")); + + const acc: AccumulatorState = { + operationsSet: new Set(), + serviceDomainsSet: new Set(), + extrasKeysSet: new Set(), + totalTools: 0, + withMetadata: 0, + toolsWithExtras: 0, + booleans: { + readOnly: { true: 0, false: 0, undefined: 0 }, + destructive: { true: 0, false: 0, undefined: 0 }, + idempotent: { true: 0, false: 0, undefined: 0 }, + openWorld: { true: 0, false: 0, undefined: 0 }, + }, + }; + + for (const file of jsonFiles) { + const data = await parseJsonFile(join(dataDir, file)); + if (!data) continue; + const tools = Array.isArray(data.tools) ? (data.tools as ToolEntry[]) : []; + accumulateTools(tools, acc); + } + + return { + coverage: { totalTools: acc.totalTools, withMetadata: acc.withMetadata }, + distinct: { + operations: Array.from(acc.operationsSet).sort(), + serviceDomains: Array.from(acc.serviceDomainsSet).sort(), + }, + booleans: acc.booleans, + extras: { + toolsWithExtras: acc.toolsWithExtras, + distinctKeys: Array.from(acc.extrasKeysSet).sort(), + }, + }; +} diff --git a/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts b/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts new file mode 100644 index 000000000..1e78a6b38 --- /dev/null +++ b/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { filterTools } from "../../../app/_components/toolkit-docs/components/available-tools-table"; + +const makeTool = ( + name: string, + behavior: Record +) => ({ + name, + qualifiedName: `Test.${name}`, + description: null, + metadata: { + classification: { serviceDomains: [] }, + behavior: { + operations: [], + readOnly: behavior.readOnly, + destructive: behavior.destructive, + idempotent: behavior.idempotent, + openWorld: behavior.openWorld, + }, + extras: null, + }, +}); + +describe("filterTools — behavior flags", () => { + const tools = [ + makeTool("ReadOnly", { readOnly: true, destructive: false }), + makeTool("Destructive", { readOnly: false, destructive: true }), + makeTool("Safe", { readOnly: false, destructive: false }), + { + name: "NoMeta", + qualifiedName: "Test.NoMeta", + description: null, + metadata: null, + }, + ]; + + it("returns all tools when no behavior flags are active", () => { + expect(filterTools(tools, "", "all")).toHaveLength(4); + }); + + it("filters to readOnly=true tools", () => { + const result = filterTools(tools, "", "all", { + behaviorFlags: { readOnly: true }, + }); + expect(result.map((tool) => tool.name)).toEqual(["ReadOnly"]); + }); + + it("filters to destructive=true tools", () => { + const result = filterTools(tools, "", "all", { + behaviorFlags: { destructive: true }, + }); + expect(result.map((tool) => tool.name)).toEqual(["Destructive"]); + }); + + it("ANDs multiple flags together", () => { + const result = filterTools(tools, "", "all", { + behaviorFlags: { readOnly: true, destructive: true }, + }); + expect(result).toHaveLength(0); + }); +}); diff --git a/toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts b/toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts new file mode 100644 index 000000000..45b7e26a3 --- /dev/null +++ b/toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { filterTools } from "../../../app/_components/toolkit-docs/components/available-tools-table"; + +const makeTool = (name: string, operations: string[]) => ({ + name, + qualifiedName: `Slack.${name}`, + description: null, + metadata: { + classification: { serviceDomains: [] }, + behavior: { + operations, + readOnly: false, + destructive: false, + idempotent: false, + openWorld: false, + }, + extras: null, + }, +}); + +describe("filterTools — operations", () => { + const tools = [ + makeTool("GetMessages", ["read"]), + makeTool("PostMessage", ["create"]), + makeTool("DeleteMessage", ["delete"]), + { + name: "NoMeta", + qualifiedName: "Slack.NoMeta", + description: null, + metadata: null, + }, + ]; + + it("returns all tools when no operations are selected", () => { + expect(filterTools(tools, "", "all")).toHaveLength(4); + }); + + it("returns only tools matching selected operations", () => { + const result = filterTools(tools, "", "all", { + activeOperations: new Set(["read"]), + }); + expect(result.map((tool) => tool.name)).toEqual(["GetMessages"]); + }); + + it("returns a union when multiple operations are selected", () => { + const result = filterTools(tools, "", "all", { + activeOperations: new Set(["read", "create"]), + }); + expect(result.map((tool) => tool.name)).toEqual([ + "GetMessages", + "PostMessage", + ]); + }); + + it("does not include tools without metadata when operation filters are active", () => { + const result = filterTools(tools, "", "all", { + activeOperations: new Set(["read"]), + }); + expect(result.map((tool) => tool.name)).not.toContain("NoMeta"); + }); +}); diff --git a/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts b/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts new file mode 100644 index 000000000..84fac90f8 --- /dev/null +++ b/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { getSharedServiceDomain } from "../../../app/_components/toolkit-docs/components/toolkit-page"; + +const makeTool = (domains: string[]) => ({ + metadata: { + classification: { serviceDomains: domains }, + behavior: { operations: [] }, + extras: null, + }, +}); + +describe("getSharedServiceDomain", () => { + it("returns the domain when all tools share exactly one", () => { + const tools = [ + makeTool(["messaging"]), + makeTool(["messaging"]), + makeTool(["messaging"]), + ]; + expect(getSharedServiceDomain(tools)).toBe("messaging"); + }); + + it("returns null when tools have different domains", () => { + const tools = [makeTool(["messaging"]), makeTool(["email"])]; + expect(getSharedServiceDomain(tools)).toBeNull(); + }); + + it("returns null when any tool has no metadata", () => { + const tools = [makeTool(["messaging"]), { metadata: null }]; + expect( + getSharedServiceDomain( + tools as Parameters[0] + ) + ).toBeNull(); + }); + + it("returns null for an empty tool list", () => { + expect(getSharedServiceDomain([])).toBeNull(); + }); + + it("returns null when a tool has multiple domains", () => { + const tools = [makeTool(["messaging", "email"]), makeTool(["messaging"])]; + expect(getSharedServiceDomain(tools)).toBeNull(); + }); +}); From 25484feae5962bf14c3c3047c00158e594154b6a Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 21:07:45 -0300 Subject: [PATCH 02/11] fix: add missing TOOL_METADATA_* exports to constants.ts The prior commit added TOOL_METADATA_SERVICE_DOMAIN_STYLES, TOOL_METADATA_OPERATION_STYLES, and TOOL_METADATA_FALLBACK_STYLE imports to toolkit-page.tsx but the corresponding exports were never committed to constants.ts, breaking the Vercel build. Made-with: Cursor --- app/_components/toolkit-docs/constants.ts | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/app/_components/toolkit-docs/constants.ts b/app/_components/toolkit-docs/constants.ts index 59e86a69f..138aee9df 100644 --- a/app/_components/toolkit-docs/constants.ts +++ b/app/_components/toolkit-docs/constants.ts @@ -176,3 +176,55 @@ export const ICON_SIZES = { stat: "h-4 w-4", inline: "h-3 w-3", } as const; + +// ============================================================================= +// Tool Metadata Style Maps +// ============================================================================= + +/** + * Tailwind classes for each operation enum value. + * Fallback to TOOL_METADATA_FALLBACK_STYLE for unknown values. + */ +export const TOOL_METADATA_OPERATION_STYLES: Record = { + read: "border-blue-500/40 bg-blue-500/10 text-blue-300", + create: "border-emerald-500/40 bg-emerald-500/10 text-emerald-300", + update: "border-amber-500/40 bg-amber-500/10 text-amber-300", + delete: "border-rose-500/40 bg-rose-500/10 text-rose-300", + opaque: "border-violet-500/40 bg-violet-500/10 text-violet-300", +}; + +/** + * Tailwind classes for each service domain enum value. + * Fallback to TOOL_METADATA_FALLBACK_STYLE for unknown values. + */ +export const TOOL_METADATA_SERVICE_DOMAIN_STYLES: Record = { + calendar: "border-blue-400/40 bg-blue-400/10 text-blue-300", + cloud_storage: "border-sky-500/40 bg-sky-500/10 text-sky-300", + code_sandbox: "border-cyan-500/40 bg-cyan-500/10 text-cyan-300", + crm: "border-indigo-500/40 bg-indigo-500/10 text-indigo-300", + customer_support: "border-teal-500/40 bg-teal-500/10 text-teal-300", + documents: "border-lime-500/40 bg-lime-500/10 text-lime-300", + ecommerce: "border-orange-500/40 bg-orange-500/10 text-orange-300", + email: "border-blue-500/40 bg-blue-500/10 text-blue-300", + financial_data: "border-emerald-500/40 bg-emerald-500/10 text-emerald-300", + geospatial: "border-green-500/40 bg-green-500/10 text-green-300", + messaging: "border-sky-500/40 bg-sky-500/10 text-sky-300", + music_streaming: "border-pink-500/40 bg-pink-500/10 text-pink-300", + payments: "border-emerald-400/40 bg-emerald-400/10 text-emerald-300", + presentations: "border-amber-500/40 bg-amber-500/10 text-amber-300", + project_management: "border-cyan-400/40 bg-cyan-400/10 text-cyan-300", + social_media: "border-violet-500/40 bg-violet-500/10 text-violet-300", + source_code: "border-slate-500/40 bg-slate-500/10 text-slate-300", + spreadsheets: "border-lime-400/40 bg-lime-400/10 text-lime-300", + travel: "border-rose-400/40 bg-rose-400/10 text-rose-300", + video_conferencing: + "border-fuchsia-500/40 bg-fuchsia-500/10 text-fuchsia-300", + video_hosting: "border-red-500/40 bg-red-500/10 text-red-300", + web_scraping: "border-zinc-500/40 bg-zinc-500/10 text-zinc-300", +}; + +/** + * Fallback style for enum values not found in the maps above. + */ +export const TOOL_METADATA_FALLBACK_STYLE = + "border-muted/60 bg-muted/20 text-muted-foreground"; From 4b05e5cd44fdd5399fe84d69abb0caf6ba18d5f7 Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 21:15:52 -0300 Subject: [PATCH 03/11] fix: address PR review findings - types and defensive guards - Add BehaviorFlagKey type export to types/index.ts (fixes Vercel build error: 'BehaviorFlagKey' not exported from types) - getSharedServiceDomain: add typeof string guard before accepting a serviceDomains entry; prevents TypeError if JSON has a non-string value calling .replace() on a Badge label (high severity) - matchesBehaviorFlags: skip entries where expected is undefined; with Partial> a key can be undefined to mean "don't filter", but the old code compared !== expected which incorrectly excluded tools (low severity) Made-with: Cursor --- .../toolkit-docs/components/available-tools-table.tsx | 5 ++++- app/_components/toolkit-docs/components/toolkit-page.tsx | 2 +- app/_components/toolkit-docs/types/index.ts | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/_components/toolkit-docs/components/available-tools-table.tsx b/app/_components/toolkit-docs/components/available-tools-table.tsx index 16abcc9d7..3238ce096 100644 --- a/app/_components/toolkit-docs/components/available-tools-table.tsx +++ b/app/_components/toolkit-docs/components/available-tools-table.tsx @@ -535,8 +535,11 @@ function matchesBehaviorFlags( ): boolean { for (const [key, expected] of Object.entries(behaviorFlags) as [ BehaviorFlagKey, - boolean, + boolean | undefined, ][]) { + if (expected === undefined) { + continue; + } if (tool.metadata?.behavior?.[key] !== expected) { return false; } diff --git a/app/_components/toolkit-docs/components/toolkit-page.tsx b/app/_components/toolkit-docs/components/toolkit-page.tsx index f8ef0726e..35050266a 100644 --- a/app/_components/toolkit-docs/components/toolkit-page.tsx +++ b/app/_components/toolkit-docs/components/toolkit-page.tsx @@ -232,7 +232,7 @@ export function getSharedServiceDomain( } const domain = serviceDomains[0]; - if (!domain) { + if (!domain || typeof domain !== "string") { return null; } diff --git a/app/_components/toolkit-docs/types/index.ts b/app/_components/toolkit-docs/types/index.ts index 423c58813..21b9e237d 100644 --- a/app/_components/toolkit-docs/types/index.ts +++ b/app/_components/toolkit-docs/types/index.ts @@ -197,6 +197,12 @@ export type ToolMetadataBehavior = { openWorld?: boolean; }; +export type BehaviorFlagKey = + | "readOnly" + | "destructive" + | "idempotent" + | "openWorld"; + export type ToolMetadata = { classification: ToolMetadataClassification; behavior: ToolMetadataBehavior; From b550d160fb4e688cfa98a51feda7e99eb3883749 Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 21:27:54 -0300 Subject: [PATCH 04/11] fix --- app/_components/toolkit-docs/types/index.ts | 1 + .../available-tools-filter-behavior.test.ts | 9 +++++++++ .../app-lib/shared-service-domain.test.ts | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/app/_components/toolkit-docs/types/index.ts b/app/_components/toolkit-docs/types/index.ts index 21b9e237d..827e13850 100644 --- a/app/_components/toolkit-docs/types/index.ts +++ b/app/_components/toolkit-docs/types/index.ts @@ -455,6 +455,7 @@ export type AvailableToolsTableProps = { secrets?: string[]; secretsInfo?: ToolSecret[]; scopes?: string[]; + metadata?: ToolMetadata | null; }>; /** Optional label for the secrets column */ secretsColumnLabel?: string; diff --git a/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts b/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts index 1e78a6b38..9bbb5d5c5 100644 --- a/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts +++ b/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts @@ -58,4 +58,13 @@ describe("filterTools — behavior flags", () => { }); expect(result).toHaveLength(0); }); + + it("ignores behavior flags explicitly set to undefined", () => { + const result = filterTools(tools, "", "all", { + behaviorFlags: { + readOnly: undefined, + }, + }); + expect(result).toHaveLength(4); + }); }); diff --git a/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts b/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts index 84fac90f8..672d03503 100644 --- a/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts +++ b/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts @@ -41,4 +41,22 @@ describe("getSharedServiceDomain", () => { const tools = [makeTool(["messaging", "email"]), makeTool(["messaging"])]; expect(getSharedServiceDomain(tools)).toBeNull(); }); + + it("returns null when domain is not a string", () => { + const tools = [ + { + metadata: { + classification: { serviceDomains: [123] }, + behavior: { operations: [] }, + extras: null, + }, + }, + makeTool(["messaging"]), + ]; + expect( + getSharedServiceDomain( + tools as Parameters[0] + ) + ).toBeNull(); + }); }); From c3042847d1fb83464cde25d692e382847f46bc81 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Feb 2026 00:32:13 +0000 Subject: [PATCH 05/11] Regenerate clean markdown files --- .../langchain/use-arcade-with-langchain-py.md | 2 +- .../setup-arcade-with-your-llm-python.md | 4 ++-- .../quickstarts/call-tool-agent.md | 23 ++++++++++--------- .../en/get-started/setup/api-keys.md | 2 +- .../get-started/setup/windows-environment.md | 2 +- .../evaluate-tools/create-evaluation-suite.md | 2 +- .../evaluate-tools/run-evaluations.md | 2 +- .../tool-basics/call-tools-mcp.md | 2 +- .../en/guides/deployment-hosting/on-prem.md | 2 +- public/_markdown/en/references/arcade-cli.md | 2 +- .../en/references/cli-cheat-sheet.md | 2 +- .../_markdown/en/references/mcp/telemetry.md | 4 ++-- 12 files changed, 25 insertions(+), 24 deletions(-) diff --git a/public/_markdown/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-py.md b/public/_markdown/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-py.md index d22c89fb4..ea118f175 100644 --- a/public/_markdown/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-py.md +++ b/public/_markdown/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-py.md @@ -857,7 +857,7 @@ if __name__ == "__main__": asyncio.run(main()) ``` -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Overview](/en/get-started/agent-frameworks/langchain/overview.md) [Setup (TypeScript)](/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-ts.md) diff --git a/public/_markdown/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python.md b/public/_markdown/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python.md index 5bda27ce3..e581c7563 100644 --- a/public/_markdown/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python.md +++ b/public/_markdown/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python.md @@ -462,7 +462,7 @@ if __name__ == "__main__": chat() ``` -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Overview](/en/get-started/agent-frameworks.md) -[Using Arcade tools](/en/get-started/agent-frameworks/crewai/use-arcade-tools.md) +[Setup Arcade tools with CrewAI](/en/get-started/agent-frameworks/crewai/use-arcade-tools.md) diff --git a/public/_markdown/en/get-started/quickstarts/call-tool-agent.md b/public/_markdown/en/get-started/quickstarts/call-tool-agent.md index efaf4edff..866c16e0d 100644 --- a/public/_markdown/en/get-started/quickstarts/call-tool-agent.md +++ b/public/_markdown/en/get-started/quickstarts/call-tool-agent.md @@ -339,16 +339,17 @@ Email metadata: In this example, we call the tool methods directly. In your real applications and , you’ll likely be letting the LLM decide which to call. Learn more about using Arcade with Frameworks in the [Frameworks](/get-started/agent-frameworks.md) section, or [how to build your own tools](/guides/create-tools/tool-basics/build-mcp-server.md). -[![Vanilla Python logo](/images/icons/python.svg) Vanilla Python MCP Client](/guides/agent-frameworks/setup-arcade-with-your-llm-python.md) -[![LangChain logo](/images/icons/langchain.svg) LangChain Agent Framework](/guides/agent-frameworks/langchain/use-arcade-tools.md) -[![CrewAI logo](https://avatars.githubusercontent.com/u/170677839?s=200&v=4) CrewAI Agent Framework](/guides/agent-frameworks/crewai/use-arcade-tools.md) -[![OpenAI Agents logo](https://avatars.githubusercontent.com/u/14957082?s=200&v=4) OpenAI Agents Agent Framework](/guides/agent-frameworks/openai-agents/overview.md) -[![Google ADK logo](https://avatars.githubusercontent.com/u/1342004?s=200&v=4) Google ADK Agent Framework](/guides/agent-frameworks/google-adk/overview.md) - -[![LangChain logo](/images/icons/langchain.svg) LangChain Agent Framework](/guides/agent-frameworks/langchain/use-arcade-tools.md) -[![Google ADK logo](https://avatars.githubusercontent.com/u/1342004?s=200&v=4) Google ADK Agent Framework](/guides/agent-frameworks/google-adk/overview.md) -[![Mastra logo](/images/icons/mastra.svg) Mastra Agent Framework](/guides/agent-frameworks/mastra/overview.md) -[![Vercel AI logo](/images/icons/vercel.svg) Vercel AI Agent Framework](/guides/agent-frameworks/vercelai.md) +[![CrewAI logo](https://avatars.githubusercontent.com/u/170677839?s=200&v=4) CrewAI Agent Framework](/en/get-started/agent-frameworks/crewai/use-arcade-tools.md) +[![Google ADK logo](https://avatars.githubusercontent.com/u/1342004?s=200&v=4) Google ADK Agent Framework](/en/get-started/agent-frameworks/google-adk/setup-python.md) +[![LangChain logo](/images/icons/langchain.svg) LangChain Agent Framework](/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-py.md) +[![OpenAI Agents logo](https://avatars.githubusercontent.com/u/14957082?s=200&v=4) OpenAI Agents Agent Framework](/en/get-started/agent-frameworks/openai-agents/setup-python.md) +[![Vanilla Python logo](/images/icons/python.svg) Vanilla Python MCP Client](/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python.md) + +[![LangChain logo](/images/icons/langchain.svg) LangChain Agent Framework](/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain-ts.md) +[![Google ADK logo](https://avatars.githubusercontent.com/u/1342004?s=200&v=4) Google ADK Agent Framework](/en/get-started/agent-frameworks/google-adk/setup-typescript.md) +[![Mastra logo](/images/icons/mastra.svg) Mastra Agent Framework](/en/get-started/agent-frameworks/mastra.md) +[![Vercel AI logo](/images/icons/vercel.svg) Vercel AI Agent Framework](/en/get-started/agent-frameworks/vercelai.md) +[![TanStack AI logo](https://avatars.githubusercontent.com/u/72518640?s=200&v=4) TanStack AI Agent Framework](/en/get-started/agent-frameworks/tanstack-ai.md) ## Full Example Code @@ -546,7 +547,7 @@ console.log( console.log(respose_send_email.output?.value); ``` -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Windows environment setup](/en/get-started/setup/windows-environment.md) [Call tools in IDE/MCP clients](/en/get-started/quickstarts/call-tool-client.md) diff --git a/public/_markdown/en/get-started/setup/api-keys.md b/public/_markdown/en/get-started/setup/api-keys.md index 97033bcf5..f263e2558 100644 --- a/public/_markdown/en/get-started/setup/api-keys.md +++ b/public/_markdown/en/get-started/setup/api-keys.md @@ -74,7 +74,7 @@ Once you have your , you can: - [Create custom tools](/guides/create-tools/tool-basics/build-mcp-server.md) -Last updated on January 30, 2026 +Last updated on February 10, 2026 [About Arcade](/en/get-started/about-arcade.md) [Connect Arcade docs to your IDE](/en/get-started/setup/connect-arcade-docs.md) diff --git a/public/_markdown/en/get-started/setup/windows-environment.md b/public/_markdown/en/get-started/setup/windows-environment.md index ab3e919cc..db1d65553 100644 --- a/public/_markdown/en/get-started/setup/windows-environment.md +++ b/public/_markdown/en/get-started/setup/windows-environment.md @@ -13,7 +13,7 @@ This page is coming soon. The team is working on comprehensive documentation for In the meantime, if you need help setting up Arcade on Windows, please reach out on [Discord](https://discord.gg/GUZEMpEZ9p) . -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Connect Arcade docs to your IDE](/en/get-started/setup/connect-arcade-docs.md) [Call tools in agents](/en/get-started/quickstarts/call-tool-agent.md) diff --git a/public/_markdown/en/guides/create-tools/evaluate-tools/create-evaluation-suite.md b/public/_markdown/en/guides/create-tools/evaluate-tools/create-evaluation-suite.md index 90f2c45c3..0326cdd30 100644 --- a/public/_markdown/en/guides/create-tools/evaluate-tools/create-evaluation-suite.md +++ b/public/_markdown/en/guides/create-tools/evaluate-tools/create-evaluation-suite.md @@ -358,7 +358,7 @@ If you want stricter suites, increase thresholds (for example `fail_threshold=0. - Compare sources with [comparative evaluations](/guides/create-tools/evaluate-tools/comparative-evaluations.md) -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Why evaluate tools?](/en/guides/create-tools/evaluate-tools/why-evaluate.md) [Run evaluations](/en/guides/create-tools/evaluate-tools/run-evaluations.md) diff --git a/public/_markdown/en/guides/create-tools/evaluate-tools/run-evaluations.md b/public/_markdown/en/guides/create-tools/evaluate-tools/run-evaluations.md index 60d283cf9..5aff388aa 100644 --- a/public/_markdown/en/guides/create-tools/evaluate-tools/run-evaluations.md +++ b/public/_markdown/en/guides/create-tools/evaluate-tools/run-evaluations.md @@ -629,7 +629,7 @@ Ensure your evaluation files: - Learn about [comparative evaluations](/guides/create-tools/evaluate-tools/comparative-evaluations.md) for comparing sources -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Create an evaluation suite](/en/guides/create-tools/evaluate-tools/create-evaluation-suite.md) [Capture mode](/en/guides/create-tools/evaluate-tools/capture-mode.md) diff --git a/public/_markdown/en/guides/create-tools/tool-basics/call-tools-mcp.md b/public/_markdown/en/guides/create-tools/tool-basics/call-tools-mcp.md index cb328722e..8eca65f97 100644 --- a/public/_markdown/en/guides/create-tools/tool-basics/call-tools-mcp.md +++ b/public/_markdown/en/guides/create-tools/tool-basics/call-tools-mcp.md @@ -240,7 +240,7 @@ Then, your client’s configuration file should look like this: For security reasons, Local HTTP servers do not currently support managed authorization and secrets. If you need to use authorization or secrets, you should use the stdio transport and configure the Arcade API key and secrets in your connection settings. If you intend to expose your HTTP to the public internet, please follow the [on-prem MCP server](/guides/deployment-hosting/on-prem.md) guide for secure remote deployment. -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Access runtime data](/en/guides/create-tools/tool-basics/runtime-data-access.md) [Organize your MCP server and tools](/en/guides/create-tools/tool-basics/organize-mcp-tools.md) diff --git a/public/_markdown/en/guides/deployment-hosting/on-prem.md b/public/_markdown/en/guides/deployment-hosting/on-prem.md index 8fff6c16c..c294ffc82 100644 --- a/public/_markdown/en/guides/deployment-hosting/on-prem.md +++ b/public/_markdown/en/guides/deployment-hosting/on-prem.md @@ -304,7 +304,7 @@ You can now test your Server by making requests using the Playground, or an MCP - [Configure secrets](/guides/create-tools/tool-basics/create-tool-secrets.md) for your Server -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Arcade Cloud](/en/guides/deployment-hosting/arcade-cloud.md) [Configure Arcade's engine](/en/guides/deployment-hosting/configure-engine.md) diff --git a/public/_markdown/en/references/arcade-cli.md b/public/_markdown/en/references/arcade-cli.md index 64c1d24de..4c5486f30 100644 --- a/public/_markdown/en/references/arcade-cli.md +++ b/public/_markdown/en/references/arcade-cli.md @@ -455,7 +455,7 @@ Usage: arcade secret [OPTIONS] COMMAND [ARGS]... ╰────────────────────────────────────────────────────────────────────────────────────╯ ``` -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Telemetry](/en/references/mcp/telemetry.md) [CLI Cheat Sheet](/en/references/cli-cheat-sheet.md) diff --git a/public/_markdown/en/references/cli-cheat-sheet.md b/public/_markdown/en/references/cli-cheat-sheet.md index aa937119b..0bfffa5a7 100644 --- a/public/_markdown/en/references/cli-cheat-sheet.md +++ b/public/_markdown/en/references/cli-cheat-sheet.md @@ -788,7 +788,7 @@ Standard development cycle for building servers: 9. **`arcade deploy`** — Deploy to cloud (requires `server.py` entrypoint) 10. **`arcade server logs -f`** — Monitor logs -Last updated on January 30, 2026 +Last updated on February 10, 2026 [Arcade CLI](/en/references/arcade-cli.md) [Contextual Access Webhook API](/en/references/contextual-access-webhook-api.md) diff --git a/public/_markdown/en/references/mcp/telemetry.md b/public/_markdown/en/references/mcp/telemetry.md index 362a95bbc..8271f4da7 100644 --- a/public/_markdown/en/references/mcp/telemetry.md +++ b/public/_markdown/en/references/mcp/telemetry.md @@ -60,7 +60,7 @@ export ARCADE_USAGE_TRACKING=0 Or to permanently opt out, you can set this environment variable in your shell’s configuration file (for example, `~/.zshrc` for zsh or `~/.bashrc` for bash). -Last updated on January 30, 2026 +Last updated on February 10, 2026 -[Settings](/en/references/mcp/python/settings.md) +[Errors](/en/references/mcp/python/errors.md) [Arcade CLI](/en/references/arcade-cli.md) From e4d18c7affc23315e03e473dc631ed9d7a90f7e8 Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 22:01:56 -0300 Subject: [PATCH 06/11] fixing issue with tool metadata --- .../toolkit-docs/components/index.ts | 4 + .../components/tool-metadata-section.tsx | 163 ++++++++++++++++++ .../toolkit-docs/components/tool-section.tsx | 2 + 3 files changed, 169 insertions(+) create mode 100644 app/_components/toolkit-docs/components/tool-metadata-section.tsx diff --git a/app/_components/toolkit-docs/components/index.ts b/app/_components/toolkit-docs/components/index.ts index ff3a14714..1b053d8f1 100644 --- a/app/_components/toolkit-docs/components/index.ts +++ b/app/_components/toolkit-docs/components/index.ts @@ -11,6 +11,10 @@ export { export { DynamicCodeBlock } from "./dynamic-code-block"; export { ParametersTable } from "./parameters-table"; export { ScopesDisplay } from "./scopes-display"; +export { + buildBehaviorRows, + ToolMetadataSection, +} from "./tool-metadata-section"; export { ToolSection } from "./tool-section"; export { ToolkitHeader } from "./toolkit-header"; export { ToolkitPage } from "./toolkit-page"; diff --git a/app/_components/toolkit-docs/components/tool-metadata-section.tsx b/app/_components/toolkit-docs/components/tool-metadata-section.tsx new file mode 100644 index 000000000..92c156526 --- /dev/null +++ b/app/_components/toolkit-docs/components/tool-metadata-section.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { Badge } from "@arcadeai/design-system"; +import { + TOOL_METADATA_FALLBACK_STYLE, + TOOL_METADATA_OPERATION_STYLES, + TOOL_METADATA_SERVICE_DOMAIN_STYLES, +} from "../constants"; +import type { ToolMetadata, ToolMetadataBehavior } from "../types"; + +type BehaviorFlagKey = "readOnly" | "destructive" | "idempotent" | "openWorld"; + +const BEHAVIOR_LABELS: Record = { + readOnly: "Read only", + destructive: "Destructive", + idempotent: "Idempotent", + openWorld: "Open world", +}; + +type BehaviorRow = { + key: BehaviorFlagKey; + label: string; + value: boolean | null; +}; + +export function buildBehaviorRows( + behavior: ToolMetadataBehavior +): readonly BehaviorRow[] { + return (Object.keys(BEHAVIOR_LABELS) as BehaviorFlagKey[]).map((key) => ({ + key, + label: BEHAVIOR_LABELS[key], + value: behavior[key] ?? null, + })); +} + +function formatEnumLabel(value: string): string { + return value.replaceAll("_", " "); +} + +function EnumBadge({ + value, + styles, +}: { + value: string; + styles: Record; +}) { + const styleClass = styles[value] ?? TOOL_METADATA_FALLBACK_STYLE; + return ( + + {formatEnumLabel(value)} + + ); +} + +function BooleanBadge({ value }: { value: boolean | null }) { + if (value === null) { + return ( + + N/A + + ); + } + + return value ? ( + + true + + ) : ( + + false + + ); +} + +export function ToolMetadataSection({ + metadata, +}: { + metadata?: ToolMetadata | null; +}) { + if (!metadata) { + return null; + } + + const behaviorRows = buildBehaviorRows(metadata.behavior); + const hasOperations = metadata.behavior.operations.length > 0; + const hasServiceDomains = metadata.classification.serviceDomains.length > 0; + const hasAnyBehaviorValue = behaviorRows.some((row) => row.value !== null); + const hasExtras = + metadata.extras != null && Object.keys(metadata.extras).length > 0; + + if ( + !(hasOperations || hasServiceDomains || hasAnyBehaviorValue || hasExtras) + ) { + return null; + } + + return ( +
+

+ Metadata +

+ +
+ {hasOperations && ( +
+ Operations: + {metadata.behavior.operations.map((operation) => ( + + ))} +
+ )} + + {hasServiceDomains && ( +
+ + Service domains: + + {metadata.classification.serviceDomains.map((domain) => ( + + ))} +
+ )} + +
+ {behaviorRows.map((row) => ( +
+ + {row.label} + + +
+ ))} +
+ + {hasExtras && ( +
+ Extras: +
+              {JSON.stringify(metadata.extras, null, 2)}
+            
+
+ )} +
+
+ ); +} diff --git a/app/_components/toolkit-docs/components/tool-section.tsx b/app/_components/toolkit-docs/components/tool-section.tsx index 879459b92..734c7507b 100644 --- a/app/_components/toolkit-docs/components/tool-section.tsx +++ b/app/_components/toolkit-docs/components/tool-section.tsx @@ -12,6 +12,7 @@ import { import { DynamicCodeBlock } from "./dynamic-code-block"; import { ParametersTable } from "./parameters-table"; import { ScopesDisplay } from "./scopes-display"; +import { ToolMetadataSection } from "./tool-metadata-section"; const COPY_FEEDBACK_MS = 2000; const JSON_PRETTY_PRINT_INDENT = 2; @@ -504,6 +505,7 @@ export function ToolSection({ tool={tool} /> + ); From b03bd5e75584b6079234c0f7ec7dad56c9d25f08 Mon Sep 17 00:00:00 2001 From: jottakka Date: Thu, 26 Feb 2026 22:15:51 -0300 Subject: [PATCH 07/11] fixing layout issue --- .../components/dynamic-code-block.tsx | 48 +++-- .../components/tool-metadata-section.tsx | 171 ++++++++++++------ .../toolkit-docs/components/tool-section.tsx | 2 +- 3 files changed, 148 insertions(+), 73 deletions(-) diff --git a/app/_components/toolkit-docs/components/dynamic-code-block.tsx b/app/_components/toolkit-docs/components/dynamic-code-block.tsx index 48e8947bc..4d559a43d 100644 --- a/app/_components/toolkit-docs/components/dynamic-code-block.tsx +++ b/app/_components/toolkit-docs/components/dynamic-code-block.tsx @@ -351,26 +351,33 @@ export function DynamicCodeBlock({ return (
{isExpanded ? ( -
-
- +
+
+
+ + Example + +
+ +
-
-
- +
+
+ {selectedLanguage.toLowerCase()} ) : ( )}
diff --git a/app/_components/toolkit-docs/components/tool-metadata-section.tsx b/app/_components/toolkit-docs/components/tool-metadata-section.tsx index 92c156526..58abf8fbd 100644 --- a/app/_components/toolkit-docs/components/tool-metadata-section.tsx +++ b/app/_components/toolkit-docs/components/tool-metadata-section.tsx @@ -1,6 +1,7 @@ "use client"; import { Badge } from "@arcadeai/design-system"; +import { Check, ChevronDown, Lightbulb, Minus, X } from "lucide-react"; import { TOOL_METADATA_FALLBACK_STYLE, TOOL_METADATA_OPERATION_STYLES, @@ -17,7 +18,14 @@ const BEHAVIOR_LABELS: Record = { openWorld: "Open world", }; -type BehaviorRow = { +const BEHAVIOR_DESCRIPTIONS: Record = { + readOnly: "Does not modify remote state.", + destructive: "May delete or overwrite remote data.", + idempotent: "Safe to retry without extra side effects.", + openWorld: "Can call out to external systems.", +}; + +export type BehaviorRow = { key: BehaviorFlagKey; label: string; value: boolean | null; @@ -34,7 +42,18 @@ export function buildBehaviorRows( } function formatEnumLabel(value: string): string { - return value.replaceAll("_", " "); + const words = value.split("_"); + return words + .map((word, index) => { + if (word === "crm") { + return "CRM"; + } + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + return word.toLowerCase(); + }) + .join(" "); } function EnumBadge({ @@ -46,7 +65,11 @@ function EnumBadge({ }) { const styleClass = styles[value] ?? TOOL_METADATA_FALLBACK_STYLE; return ( - + + {formatEnumLabel(value)} ); @@ -56,27 +79,27 @@ function BooleanBadge({ value }: { value: boolean | null }) { if (value === null) { return ( - N/A + Unknown ); } return value ? ( - true + Yes ) : ( - false + No ); } @@ -104,58 +127,98 @@ export function ToolMetadataSection({ } return ( -
-

- Metadata -

- -
- {hasOperations && ( -
- Operations: - {metadata.behavior.operations.map((operation) => ( - - ))} +
+
+
+ +
+
+

+ Execution hints +

+

+ Signals for MCP clients and agents about how this tool behaves. +

+
+
+ +
+ {(hasOperations || hasServiceDomains) && ( +
+ {hasOperations && ( +
+ + Operations + +
+ {metadata.behavior.operations.map((operation) => ( + + ))} +
+
+ )} + + {hasServiceDomains && ( +
+ + Service domains + +
+ {metadata.classification.serviceDomains.map((domain) => ( + + ))} +
+
+ )}
)} - {hasServiceDomains && ( -
- - Service domains: + {hasAnyBehaviorValue && ( +
+ + MCP behavior - {metadata.classification.serviceDomains.map((domain) => ( - - ))} +
+ {behaviorRows.map((row) => ( +
+
+ + {row.label} + +
+ +
+
+

+ {BEHAVIOR_DESCRIPTIONS[row.key]} +

+
+ ))} +
)} -
- {behaviorRows.map((row) => ( -
- - {row.label} - - -
- ))} -
- {hasExtras && ( -
- Extras: -
+          
+ + Additional metadata + + +
               {JSON.stringify(metadata.extras, null, 2)}
             
-
+ )}
diff --git a/app/_components/toolkit-docs/components/tool-section.tsx b/app/_components/toolkit-docs/components/tool-section.tsx index 734c7507b..0f22427b9 100644 --- a/app/_components/toolkit-docs/components/tool-section.tsx +++ b/app/_components/toolkit-docs/components/tool-section.tsx @@ -485,6 +485,7 @@ export function ToolSection({ showSelection={showSelection} tool={tool} /> + - ); From 2c5b17a635f11ee53cccb9d5f3f3118477383bf8 Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 27 Feb 2026 12:54:49 -0300 Subject: [PATCH 08/11] updates after review --- .gitignore | 3 - CLAUDE.md | 2 +- package.json | 3 +- scripts/pagefind.ts | 2 +- .../scripts/generate-toolkit-markdown.ts | 92 ---- .../scripts/pagefind-toolkit-content.ts | 463 +----------------- .../scripts/generate-toolkit-markdown.test.ts | 104 ---- 7 files changed, 4 insertions(+), 665 deletions(-) delete mode 100644 toolkit-docs-generator/scripts/generate-toolkit-markdown.ts delete mode 100644 toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts diff --git a/.gitignore b/.gitignore index 8e9f00c4e..2df2b0183 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,5 @@ styles/write-good/ toolkit-docs-generator/overview-input/ toolkit-docs-generator-verification/logs/ -# Generated toolkit markdown (built at build time, not committed) -public/toolkit-markdown/ - # Git worktrees .worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md index b09bc8fbe..0767ad63a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Arcade documentation site built with Next.js + Nextra (App Router), using pnpm a ```bash pnpm dev # Local dev server (port 3000) -pnpm build # Full production build (toolkit-markdown → next build → pagefind) +pnpm build # Full production build (next build → pagefind) pnpm lint # Lint with Ultracite (Biome-based) pnpm format # Auto-format with Ultracite pnpm test # Run all Vitest tests diff --git a/package.json b/package.json index 8528807f0..f98e20d7d 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,11 @@ "type": "module", "scripts": { "dev": "next dev --webpack", - "build": "pnpm run toolkit-markdown && next build --webpack && pnpm run custompagefind", + "build": "next build --webpack && pnpm run custompagefind", "start": "next start", "lint": "pnpm exec ultracite check", "format": "pnpm exec ultracite fix", "prepare": "husky install", - "toolkit-markdown": "pnpm dlx tsx toolkit-docs-generator/scripts/generate-toolkit-markdown.ts", "postbuild": "if [ \"$SKIP_POSTBUILD\" != \"true\" ]; then pnpm run generate:markdown && pnpm run custompagefind; fi", "generate:markdown": "pnpm dlx tsx scripts/generate-clean-markdown.ts", "translate": "pnpm dlx tsx scripts/i18n-sync/index.ts && pnpm format", diff --git a/scripts/pagefind.ts b/scripts/pagefind.ts index 5e8e912be..e3957b0ce 100644 --- a/scripts/pagefind.ts +++ b/scripts/pagefind.ts @@ -8,7 +8,7 @@ import { remark } from "remark"; import remarkRehype from "remark-rehype"; import { readToolkitData } from "@/app/_lib/toolkit-data"; import { listToolkitRoutes } from "@/app/_lib/toolkit-static-params"; -import { toolkitDataToSearchMarkdown } from "../toolkit-docs-generator/scripts/pagefind-toolkit-content"; +import { toolkitDataToSearchMarkdown } from "@/app/_lib/toolkit-markdown"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts b/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts deleted file mode 100644 index 709460f1b..000000000 --- a/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { mkdir, rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { readToolkitData } from "../../app/_lib/toolkit-data"; -import { listToolkitRoutes } from "../../app/_lib/toolkit-static-params"; - -import { toolkitDataToSearchMarkdown } from "./pagefind-toolkit-content"; - -const DEFAULT_OUTPUT_ROOT = join(process.cwd(), "public", "toolkit-markdown"); - -export type GenerateToolkitMarkdownOptions = { - outputRoot?: string; - routes?: Array<{ category: string; toolkitId: string }>; - readToolkitDataFn?: typeof readToolkitData; - listToolkitRoutesFn?: typeof listToolkitRoutes; - toMarkdown?: (toolkit: Awaited>) => string; - failOnEmptyRoutes?: boolean; -}; - -type GenerateResult = { - outputRoot: string; - written: number; - skipped: number; -}; - -const shouldFailOnEmptyRoutes = (flag?: boolean): boolean => - flag ?? Boolean(process.env.CI || process.env.VERCEL); - -async function ensureCleanOutput(outputRoot: string) { - await rm(outputRoot, { recursive: true, force: true }); - await mkdir(outputRoot, { recursive: true }); -} - -export async function generateToolkitMarkdown( - options: GenerateToolkitMarkdownOptions = {} -): Promise { - const outputRoot = options.outputRoot ?? DEFAULT_OUTPUT_ROOT; - const routes = - options.routes ?? - (await (options.listToolkitRoutesFn ?? listToolkitRoutes)()); - const failOnEmpty = shouldFailOnEmptyRoutes(options.failOnEmptyRoutes); - - if (routes.length === 0) { - const message = "No toolkit routes found. Skipping markdown generation."; - if (failOnEmpty) { - throw new Error(message); - } - console.warn(message); - return { outputRoot, written: 0, skipped: 0 }; - } - - await ensureCleanOutput(outputRoot); - - let written = 0; - let skipped = 0; - - for (const route of routes) { - const data = await (options.readToolkitDataFn ?? readToolkitData)( - route.toolkitId - ); - if (!data) { - skipped += 1; - continue; - } - - const markdown = (options.toMarkdown ?? toolkitDataToSearchMarkdown)(data); - const outputDir = join(outputRoot, route.category); - const outputPath = join(outputDir, `${route.toolkitId}.md`); - - await mkdir(outputDir, { recursive: true }); - await writeFile(outputPath, markdown, "utf-8"); - written += 1; - } - - if (written === 0 && failOnEmpty) { - throw new Error( - "No toolkit markdown generated. Check toolkit-docs-generator/data/toolkits output." - ); - } - - console.log(`Generated ${written} toolkit markdown files.`); - return { outputRoot, written, skipped }; -} - -async function main() { - await generateToolkitMarkdown(); -} - -main().catch((error) => { - console.error("Failed to generate toolkit markdown:", error); - process.exitCode = 1; -}); diff --git a/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts b/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts index 47bbb6746..97ec83191 100644 --- a/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts +++ b/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts @@ -1,462 +1 @@ -import type { - ToolAuth, - ToolCodeExample, - ToolDefinition, - ToolkitAuth, - ToolkitData, - ToolMetadata, - ToolParameter, - ToolSecret, -} from "../../app/_components/toolkit-docs/types"; - -const TOOL_LIMIT = 500; - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Format a parameter type with innerType for arrays - */ -function formatParameterType(param: ToolParameter): string { - if (param.type === "array" && param.innerType) { - return `array[${param.innerType}]`; - } - return param.type; -} - -/** - * Format authentication information for toolkit level - */ -function formatToolkitAuth(auth: ToolkitAuth | null): string[] { - if (!auth) { - return []; - } - - const lines: string[] = ["", "## Authentication", ""]; - lines.push(getToolkitAuthTypeLabel(auth.type)); - appendProviderLine(lines, auth.providerId); - appendScopesSection(lines, auth.type, auth.allScopes); - - return lines; -} - -function getToolkitAuthTypeLabel(authType: ToolkitAuth["type"]): string { - if (authType === "oauth2") { - return "**Type:** OAuth 2.0"; - } - if (authType === "api_key") { - return "**Type:** API Key"; - } - return "**Type:** Mixed (OAuth 2.0 and API Keys)"; -} - -function appendProviderLine(lines: string[], providerId?: string | null): void { - if (providerId) { - lines.push(`**Provider:** ${providerId}`); - } -} - -function appendScopesSection( - lines: string[], - authType: ToolkitAuth["type"], - allScopes?: string[] | null -): void { - if (!allScopes || allScopes.length === 0) { - return; - } - - if (authType === "oauth2") { - lines.push("", "**Required scopes:**"); - } else if (authType === "mixed") { - lines.push("", "**OAuth scopes used:**"); - } else { - return; - } - - for (const scope of allScopes) { - lines.push(`- \`${scope}\``); - } -} - -/** - * Format tool-level authentication (scopes) - */ -function formatToolAuth(auth: ToolAuth | null): string[] { - if (!auth?.scopes || auth.scopes.length === 0) { - return []; - } - - const lines: string[] = [ - "", - "**OAuth Scopes:**", - ...auth.scopes.map((scope) => `- \`${scope}\``), - ]; - - return lines; -} - -/** - * Format secrets information - */ -function formatSecrets( - secrets: string[], - secretsInfo?: ToolSecret[] -): string[] { - if (!secrets || secrets.length === 0) { - return []; - } - - const lines: string[] = ["", "**Required Secrets:**"]; - - for (const secretName of secrets) { - const info = secretsInfo?.find((s) => s.name === secretName); - const typeLabel = info?.type ? ` (${info.type})` : ""; - lines.push(`- \`${secretName}\`${typeLabel}`); - } - - return lines; -} - -/** - * Format parameters as a detailed schema - */ -function formatParameters(parameters: ToolParameter[]): string[] { - if (!parameters || parameters.length === 0) { - return ["", "**Parameters:** None"]; - } - - const lines: string[] = ["", "**Parameters:**", ""]; - - for (const param of parameters) { - const requiredLabel = param.required ? " *(required)*" : " *(optional)*"; - const typeStr = formatParameterType(param); - - lines.push(`- \`${param.name}\`: \`${typeStr}\`${requiredLabel}`); - - if (param.description) { - lines.push(` - ${param.description}`); - } - - if (param.enum && param.enum.length > 0) { - lines.push( - ` - Allowed values: ${param.enum.map((v) => `\`${v}\``).join(", ")}` - ); - } - - if (param.default !== undefined) { - lines.push(` - Default: \`${JSON.stringify(param.default)}\``); - } - } - - return lines; -} - -/** - * Format output schema - */ -function formatOutput( - output: { type: string; description: string | null } | null -): string[] { - if (!output) { - return []; - } - - const lines: string[] = ["", `**Output:** \`${output.type}\``]; - if (output.description) { - lines.push(`- ${output.description}`); - } - - return lines; -} - -/** - * Format code example as JSON schema with full tool metadata - */ -function formatCodeExample( - codeExample: ToolCodeExample | undefined, - auth: ToolAuth | null, - secrets: string[], - secretsInfo?: ToolSecret[] -): string[] { - if (!codeExample) { - return []; - } - - const lines: string[] = ["", "**Example:**", ""]; - - // Create a simplified example object for parameters - const exampleParams: Record = {}; - for (const [key, val] of Object.entries(codeExample.parameters)) { - exampleParams[key] = val.value; - } - - // Build the full tool JSON schema - const toolSchema: Record = { - tool: codeExample.toolName, - parameters: exampleParams, - }; - - // Add authentication info - const hasAuthScopes = Boolean(auth?.scopes && auth.scopes.length > 0); - if (codeExample.requiresAuth || hasAuthScopes) { - toolSchema.requires_auth = codeExample.requiresAuth || hasAuthScopes; - if (codeExample.authProvider) { - toolSchema.auth_provider = codeExample.authProvider; - } - if (hasAuthScopes) { - toolSchema.scopes = auth.scopes; - } - } - - // Add secrets info - if (secrets && secrets.length > 0) { - const secretsWithTypes = secrets.map((secretName) => { - const info = secretsInfo?.find((s) => s.name === secretName); - return { - name: secretName, - type: info?.type ?? "unknown", - }; - }); - toolSchema.secrets = secretsWithTypes; - } - - const exampleJson = JSON.stringify(toolSchema, null, 2); - - lines.push("```json", exampleJson, "```"); - - return lines; -} - -/** - * Format per-tool metadata section - */ -function formatToolMetadata( - metadata: ToolMetadata | null | undefined -): string[] { - if (!metadata) return []; - - const details: string[] = []; - if (metadata.classification.serviceDomains.length > 0) { - details.push( - `- Service domains: ${metadata.classification.serviceDomains.join(", ")}` - ); - } - if (metadata.behavior.operations.length > 0) { - details.push(`- Operations: ${metadata.behavior.operations.join(", ")}`); - } - const flags: string[] = []; - if (metadata.behavior.readOnly === true) flags.push("read-only"); - if (metadata.behavior.destructive === true) flags.push("destructive"); - if (metadata.behavior.idempotent === true) flags.push("idempotent"); - if (metadata.behavior.openWorld === true) flags.push("open-world"); - if (flags.length > 0) { - details.push(`- Flags: ${flags.join(", ")}`); - } - if (metadata.extras && Object.keys(metadata.extras).length > 0) { - details.push(`- Extras: ${JSON.stringify(metadata.extras)}`); - } - - return details.length > 0 ? ["", "**Tool Metadata:**", ...details] : []; -} - -/** - * Format toolkit metadata section - */ -function buildToolkitMetadataSection(toolkit: ToolkitData): string[] { - const { metadata } = toolkit; - if (!metadata) return []; - - const lines: string[] = ["", "## Toolkit Info", ""]; - lines.push(`**Category:** ${metadata.category}`); - lines.push(`**Type:** ${metadata.type}`); - if (metadata.docsLink) { - lines.push(`**Documentation:** ${metadata.docsLink}`); - } - const flags: string[] = []; - if (metadata.isBYOC) flags.push("BYOC"); - if (metadata.isPro) flags.push("Pro"); - if (metadata.isHidden) flags.push("Hidden"); - if (flags.length > 0) { - lines.push(`**Flags:** ${flags.join(", ")}`); - } - return lines; -} - -/** - * Format a complete tool definition with all details - */ -function formatToolDefinition(tool: ToolDefinition): string[] { - const lines: string[] = []; - - // Tool header - lines.push(`### ${tool.qualifiedName}`); - lines.push(""); - - // Version info - if ( - tool.fullyQualifiedName && - tool.fullyQualifiedName !== tool.qualifiedName - ) { - const version = tool.fullyQualifiedName.split("@")[1]; - if (version) { - lines.push(`**Version:** ${version}`); - } - } - - // Description - if (tool.description) { - lines.push(""); - lines.push(tool.description); - } - - // Parameters - lines.push(...formatParameters(tool.parameters)); - - // Auth scopes - lines.push(...formatToolAuth(tool.auth)); - - // Secrets - lines.push(...formatSecrets(tool.secrets, tool.secretsInfo)); - - // Output - lines.push(...formatOutput(tool.output)); - - // Tool metadata - lines.push(...formatToolMetadata(tool.metadata)); - - // Code example with auth and secrets - lines.push( - ...formatCodeExample( - tool.codeExample, - tool.auth, - tool.secrets, - tool.secretsInfo - ) - ); - - lines.push(""); - - return lines; -} - -// ============================================================================ -// Main Export -// ============================================================================ - -/** - * Convert toolkit data to comprehensive markdown documentation - * Includes all available information: parameters, auth, secrets, examples - */ -export function toolkitDataToSearchMarkdown(toolkit: ToolkitData): string { - const title = toolkit.label || toolkit.id; - const sections: string[] = buildToolkitHeaderSections(toolkit, title); - sections.push(...formatToolkitAuth(toolkit.auth)); - sections.push(...buildToolkitMetadataSection(toolkit)); - sections.push(...buildDocumentationChunkSections(toolkit)); - - const toolsToInclude = toolkit.tools.slice(0, TOOL_LIMIT); - const truncatedCount = Math.max( - 0, - toolkit.tools.length - toolsToInclude.length - ); - - sections.push( - ...buildToolsSummarySections( - toolkit.tools.length, - toolsToInclude, - truncatedCount - ) - ); - sections.push(...buildToolDetailsSections(toolsToInclude)); - sections.push(...buildGeneratedAtSection(toolkit.generatedAt)); - - return sections.join("\n"); -} - -function buildToolkitHeaderSections( - toolkit: ToolkitData, - title: string -): string[] { - const sections: string[] = [`# ${title}`]; - - if (toolkit.version) { - sections.push(`**Version:** ${toolkit.version}`); - } - - if (toolkit.description) { - sections.push("", toolkit.description); - } - - if (toolkit.summary) { - sections.push("", "## Overview", "", toolkit.summary); - } - - return sections; -} - -function buildDocumentationChunkSections(toolkit: ToolkitData): string[] { - if ( - !toolkit.documentationChunks || - toolkit.documentationChunks.length === 0 - ) { - return []; - } - - const sections: string[] = []; - for (const chunk of toolkit.documentationChunks) { - if (chunk.content) { - sections.push("", chunk.content); - } - } - return sections; -} - -function buildToolsSummarySections( - totalTools: number, - toolsToInclude: ToolDefinition[], - truncatedCount: number -): string[] { - const sections: string[] = [ - "", - "## Tools", - "", - `This toolkit provides **${totalTools} tools**:`, - "", - "### Quick Reference", - "", - ]; - - for (const tool of toolsToInclude) { - sections.push(buildQuickReferenceLine(tool)); - } - - if (truncatedCount > 0) { - sections.push("", `*... and ${truncatedCount} more tools.*`); - } - - return sections; -} - -function buildQuickReferenceLine(tool: ToolDefinition): string { - const description = tool.description - ? ` — ${tool.description.split("\n")[0]}` - : ""; - const anchor = tool.qualifiedName.toLowerCase().replace(/\./g, ""); - return `- [\`${tool.qualifiedName}\`](#${anchor})${description}`; -} - -function buildToolDetailsSections(toolsToInclude: ToolDefinition[]): string[] { - const sections: string[] = ["", "---", "", "## Tool Details", ""]; - for (const tool of toolsToInclude) { - sections.push(...formatToolDefinition(tool), "---", ""); - } - return sections; -} - -function buildGeneratedAtSection(generatedAt?: string): string[] { - if (!generatedAt) { - return []; - } - return ["", `*Documentation generated: ${generatedAt}*`]; -} +export { toolkitDataToSearchMarkdown } from "../../app/_lib/toolkit-markdown"; diff --git a/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts b/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts deleted file mode 100644 index 4639a4f9d..000000000 --- a/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import type { ToolkitData } from "../../../app/_components/toolkit-docs/types"; -import { generateToolkitMarkdown } from "../../scripts/generate-toolkit-markdown"; - -const createToolkit = (overrides: Partial = {}): ToolkitData => ({ - id: "Github", - label: "GitHub", - version: "1.0.0", - description: null, - metadata: { - category: "development", - iconUrl: "https://design-system.arcade.dev/icons/github.svg", - isBYOC: false, - isPro: false, - type: "arcade", - docsLink: "https://docs.arcade.dev", - isComingSoon: false, - isHidden: false, - }, - auth: null, - tools: [], - documentationChunks: [], - customImports: [], - subPages: [], - generatedAt: new Date().toISOString(), - ...overrides, -}); - -const createTempDir = async (): Promise => - mkdtemp(join(tmpdir(), "toolkit-markdown-")); - -const cleanupTempDir = async (dir: string | null) => { - if (dir) { - await rm(dir, { recursive: true, force: true }); - } -}; - -describe("generateToolkitMarkdown", () => { - let tempDir: string | null = null; - - afterEach(async () => { - await cleanupTempDir(tempDir); - tempDir = null; - }); - - it("fails when no routes exist and failOnEmptyRoutes is true", async () => { - tempDir = await createTempDir(); - await expect( - generateToolkitMarkdown({ - routes: [], - outputRoot: tempDir, - failOnEmptyRoutes: true, - }) - ).rejects.toThrow("No toolkit routes found"); - }); - - it("writes markdown for available toolkits and skips missing data", async () => { - tempDir = await createTempDir(); - - const routes = [ - { category: "development", toolkitId: "github" }, - { category: "sales", toolkitId: "hubspot" }, - ]; - - const result = await generateToolkitMarkdown({ - outputRoot: tempDir, - routes, - readToolkitDataFn: (toolkitId: string) => { - if (toolkitId === "github") { - return Promise.resolve( - createToolkit({ id: "Github", label: "GitHub" }) - ); - } - return Promise.resolve(null); - }, - toMarkdown: () => "# Generated\n", - failOnEmptyRoutes: false, - }); - - const outputPath = join(tempDir, "development", "github.md"); - const content = await readFile(outputPath, "utf-8"); - - expect(content).toBe("# Generated\n"); - expect(result.written).toBe(1); - expect(result.skipped).toBe(1); - }); - - it("fails when no markdown is written and failOnEmptyRoutes is true", async () => { - tempDir = await createTempDir(); - - await expect( - generateToolkitMarkdown({ - outputRoot: tempDir, - routes: [{ category: "development", toolkitId: "github" }], - readToolkitDataFn: async () => null, - toMarkdown: () => "# Generated\n", - failOnEmptyRoutes: true, - }) - ).rejects.toThrow("No toolkit markdown generated"); - }); -}); From 0def562bf90e717996d4e72d8712e2a1597bc582 Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 27 Feb 2026 16:11:35 -0300 Subject: [PATCH 09/11] Revert "updates after review" This reverts commit 2c5b17a635f11ee53cccb9d5f3f3118477383bf8. --- .gitignore | 3 + CLAUDE.md | 2 +- package.json | 3 +- scripts/pagefind.ts | 2 +- .../scripts/generate-toolkit-markdown.ts | 92 ++++ .../scripts/pagefind-toolkit-content.ts | 463 +++++++++++++++++- .../scripts/generate-toolkit-markdown.test.ts | 104 ++++ 7 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 toolkit-docs-generator/scripts/generate-toolkit-markdown.ts create mode 100644 toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts diff --git a/.gitignore b/.gitignore index 2df2b0183..8e9f00c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,8 @@ styles/write-good/ toolkit-docs-generator/overview-input/ toolkit-docs-generator-verification/logs/ +# Generated toolkit markdown (built at build time, not committed) +public/toolkit-markdown/ + # Git worktrees .worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md index 0767ad63a..b09bc8fbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Arcade documentation site built with Next.js + Nextra (App Router), using pnpm a ```bash pnpm dev # Local dev server (port 3000) -pnpm build # Full production build (next build → pagefind) +pnpm build # Full production build (toolkit-markdown → next build → pagefind) pnpm lint # Lint with Ultracite (Biome-based) pnpm format # Auto-format with Ultracite pnpm test # Run all Vitest tests diff --git a/package.json b/package.json index f98e20d7d..8528807f0 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "dev": "next dev --webpack", - "build": "next build --webpack && pnpm run custompagefind", + "build": "pnpm run toolkit-markdown && next build --webpack && pnpm run custompagefind", "start": "next start", "lint": "pnpm exec ultracite check", "format": "pnpm exec ultracite fix", "prepare": "husky install", + "toolkit-markdown": "pnpm dlx tsx toolkit-docs-generator/scripts/generate-toolkit-markdown.ts", "postbuild": "if [ \"$SKIP_POSTBUILD\" != \"true\" ]; then pnpm run generate:markdown && pnpm run custompagefind; fi", "generate:markdown": "pnpm dlx tsx scripts/generate-clean-markdown.ts", "translate": "pnpm dlx tsx scripts/i18n-sync/index.ts && pnpm format", diff --git a/scripts/pagefind.ts b/scripts/pagefind.ts index e3957b0ce..5e8e912be 100644 --- a/scripts/pagefind.ts +++ b/scripts/pagefind.ts @@ -8,7 +8,7 @@ import { remark } from "remark"; import remarkRehype from "remark-rehype"; import { readToolkitData } from "@/app/_lib/toolkit-data"; import { listToolkitRoutes } from "@/app/_lib/toolkit-static-params"; -import { toolkitDataToSearchMarkdown } from "@/app/_lib/toolkit-markdown"; +import { toolkitDataToSearchMarkdown } from "../toolkit-docs-generator/scripts/pagefind-toolkit-content"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts b/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts new file mode 100644 index 000000000..709460f1b --- /dev/null +++ b/toolkit-docs-generator/scripts/generate-toolkit-markdown.ts @@ -0,0 +1,92 @@ +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { readToolkitData } from "../../app/_lib/toolkit-data"; +import { listToolkitRoutes } from "../../app/_lib/toolkit-static-params"; + +import { toolkitDataToSearchMarkdown } from "./pagefind-toolkit-content"; + +const DEFAULT_OUTPUT_ROOT = join(process.cwd(), "public", "toolkit-markdown"); + +export type GenerateToolkitMarkdownOptions = { + outputRoot?: string; + routes?: Array<{ category: string; toolkitId: string }>; + readToolkitDataFn?: typeof readToolkitData; + listToolkitRoutesFn?: typeof listToolkitRoutes; + toMarkdown?: (toolkit: Awaited>) => string; + failOnEmptyRoutes?: boolean; +}; + +type GenerateResult = { + outputRoot: string; + written: number; + skipped: number; +}; + +const shouldFailOnEmptyRoutes = (flag?: boolean): boolean => + flag ?? Boolean(process.env.CI || process.env.VERCEL); + +async function ensureCleanOutput(outputRoot: string) { + await rm(outputRoot, { recursive: true, force: true }); + await mkdir(outputRoot, { recursive: true }); +} + +export async function generateToolkitMarkdown( + options: GenerateToolkitMarkdownOptions = {} +): Promise { + const outputRoot = options.outputRoot ?? DEFAULT_OUTPUT_ROOT; + const routes = + options.routes ?? + (await (options.listToolkitRoutesFn ?? listToolkitRoutes)()); + const failOnEmpty = shouldFailOnEmptyRoutes(options.failOnEmptyRoutes); + + if (routes.length === 0) { + const message = "No toolkit routes found. Skipping markdown generation."; + if (failOnEmpty) { + throw new Error(message); + } + console.warn(message); + return { outputRoot, written: 0, skipped: 0 }; + } + + await ensureCleanOutput(outputRoot); + + let written = 0; + let skipped = 0; + + for (const route of routes) { + const data = await (options.readToolkitDataFn ?? readToolkitData)( + route.toolkitId + ); + if (!data) { + skipped += 1; + continue; + } + + const markdown = (options.toMarkdown ?? toolkitDataToSearchMarkdown)(data); + const outputDir = join(outputRoot, route.category); + const outputPath = join(outputDir, `${route.toolkitId}.md`); + + await mkdir(outputDir, { recursive: true }); + await writeFile(outputPath, markdown, "utf-8"); + written += 1; + } + + if (written === 0 && failOnEmpty) { + throw new Error( + "No toolkit markdown generated. Check toolkit-docs-generator/data/toolkits output." + ); + } + + console.log(`Generated ${written} toolkit markdown files.`); + return { outputRoot, written, skipped }; +} + +async function main() { + await generateToolkitMarkdown(); +} + +main().catch((error) => { + console.error("Failed to generate toolkit markdown:", error); + process.exitCode = 1; +}); diff --git a/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts b/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts index 97ec83191..47bbb6746 100644 --- a/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts +++ b/toolkit-docs-generator/scripts/pagefind-toolkit-content.ts @@ -1 +1,462 @@ -export { toolkitDataToSearchMarkdown } from "../../app/_lib/toolkit-markdown"; +import type { + ToolAuth, + ToolCodeExample, + ToolDefinition, + ToolkitAuth, + ToolkitData, + ToolMetadata, + ToolParameter, + ToolSecret, +} from "../../app/_components/toolkit-docs/types"; + +const TOOL_LIMIT = 500; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Format a parameter type with innerType for arrays + */ +function formatParameterType(param: ToolParameter): string { + if (param.type === "array" && param.innerType) { + return `array[${param.innerType}]`; + } + return param.type; +} + +/** + * Format authentication information for toolkit level + */ +function formatToolkitAuth(auth: ToolkitAuth | null): string[] { + if (!auth) { + return []; + } + + const lines: string[] = ["", "## Authentication", ""]; + lines.push(getToolkitAuthTypeLabel(auth.type)); + appendProviderLine(lines, auth.providerId); + appendScopesSection(lines, auth.type, auth.allScopes); + + return lines; +} + +function getToolkitAuthTypeLabel(authType: ToolkitAuth["type"]): string { + if (authType === "oauth2") { + return "**Type:** OAuth 2.0"; + } + if (authType === "api_key") { + return "**Type:** API Key"; + } + return "**Type:** Mixed (OAuth 2.0 and API Keys)"; +} + +function appendProviderLine(lines: string[], providerId?: string | null): void { + if (providerId) { + lines.push(`**Provider:** ${providerId}`); + } +} + +function appendScopesSection( + lines: string[], + authType: ToolkitAuth["type"], + allScopes?: string[] | null +): void { + if (!allScopes || allScopes.length === 0) { + return; + } + + if (authType === "oauth2") { + lines.push("", "**Required scopes:**"); + } else if (authType === "mixed") { + lines.push("", "**OAuth scopes used:**"); + } else { + return; + } + + for (const scope of allScopes) { + lines.push(`- \`${scope}\``); + } +} + +/** + * Format tool-level authentication (scopes) + */ +function formatToolAuth(auth: ToolAuth | null): string[] { + if (!auth?.scopes || auth.scopes.length === 0) { + return []; + } + + const lines: string[] = [ + "", + "**OAuth Scopes:**", + ...auth.scopes.map((scope) => `- \`${scope}\``), + ]; + + return lines; +} + +/** + * Format secrets information + */ +function formatSecrets( + secrets: string[], + secretsInfo?: ToolSecret[] +): string[] { + if (!secrets || secrets.length === 0) { + return []; + } + + const lines: string[] = ["", "**Required Secrets:**"]; + + for (const secretName of secrets) { + const info = secretsInfo?.find((s) => s.name === secretName); + const typeLabel = info?.type ? ` (${info.type})` : ""; + lines.push(`- \`${secretName}\`${typeLabel}`); + } + + return lines; +} + +/** + * Format parameters as a detailed schema + */ +function formatParameters(parameters: ToolParameter[]): string[] { + if (!parameters || parameters.length === 0) { + return ["", "**Parameters:** None"]; + } + + const lines: string[] = ["", "**Parameters:**", ""]; + + for (const param of parameters) { + const requiredLabel = param.required ? " *(required)*" : " *(optional)*"; + const typeStr = formatParameterType(param); + + lines.push(`- \`${param.name}\`: \`${typeStr}\`${requiredLabel}`); + + if (param.description) { + lines.push(` - ${param.description}`); + } + + if (param.enum && param.enum.length > 0) { + lines.push( + ` - Allowed values: ${param.enum.map((v) => `\`${v}\``).join(", ")}` + ); + } + + if (param.default !== undefined) { + lines.push(` - Default: \`${JSON.stringify(param.default)}\``); + } + } + + return lines; +} + +/** + * Format output schema + */ +function formatOutput( + output: { type: string; description: string | null } | null +): string[] { + if (!output) { + return []; + } + + const lines: string[] = ["", `**Output:** \`${output.type}\``]; + if (output.description) { + lines.push(`- ${output.description}`); + } + + return lines; +} + +/** + * Format code example as JSON schema with full tool metadata + */ +function formatCodeExample( + codeExample: ToolCodeExample | undefined, + auth: ToolAuth | null, + secrets: string[], + secretsInfo?: ToolSecret[] +): string[] { + if (!codeExample) { + return []; + } + + const lines: string[] = ["", "**Example:**", ""]; + + // Create a simplified example object for parameters + const exampleParams: Record = {}; + for (const [key, val] of Object.entries(codeExample.parameters)) { + exampleParams[key] = val.value; + } + + // Build the full tool JSON schema + const toolSchema: Record = { + tool: codeExample.toolName, + parameters: exampleParams, + }; + + // Add authentication info + const hasAuthScopes = Boolean(auth?.scopes && auth.scopes.length > 0); + if (codeExample.requiresAuth || hasAuthScopes) { + toolSchema.requires_auth = codeExample.requiresAuth || hasAuthScopes; + if (codeExample.authProvider) { + toolSchema.auth_provider = codeExample.authProvider; + } + if (hasAuthScopes) { + toolSchema.scopes = auth.scopes; + } + } + + // Add secrets info + if (secrets && secrets.length > 0) { + const secretsWithTypes = secrets.map((secretName) => { + const info = secretsInfo?.find((s) => s.name === secretName); + return { + name: secretName, + type: info?.type ?? "unknown", + }; + }); + toolSchema.secrets = secretsWithTypes; + } + + const exampleJson = JSON.stringify(toolSchema, null, 2); + + lines.push("```json", exampleJson, "```"); + + return lines; +} + +/** + * Format per-tool metadata section + */ +function formatToolMetadata( + metadata: ToolMetadata | null | undefined +): string[] { + if (!metadata) return []; + + const details: string[] = []; + if (metadata.classification.serviceDomains.length > 0) { + details.push( + `- Service domains: ${metadata.classification.serviceDomains.join(", ")}` + ); + } + if (metadata.behavior.operations.length > 0) { + details.push(`- Operations: ${metadata.behavior.operations.join(", ")}`); + } + const flags: string[] = []; + if (metadata.behavior.readOnly === true) flags.push("read-only"); + if (metadata.behavior.destructive === true) flags.push("destructive"); + if (metadata.behavior.idempotent === true) flags.push("idempotent"); + if (metadata.behavior.openWorld === true) flags.push("open-world"); + if (flags.length > 0) { + details.push(`- Flags: ${flags.join(", ")}`); + } + if (metadata.extras && Object.keys(metadata.extras).length > 0) { + details.push(`- Extras: ${JSON.stringify(metadata.extras)}`); + } + + return details.length > 0 ? ["", "**Tool Metadata:**", ...details] : []; +} + +/** + * Format toolkit metadata section + */ +function buildToolkitMetadataSection(toolkit: ToolkitData): string[] { + const { metadata } = toolkit; + if (!metadata) return []; + + const lines: string[] = ["", "## Toolkit Info", ""]; + lines.push(`**Category:** ${metadata.category}`); + lines.push(`**Type:** ${metadata.type}`); + if (metadata.docsLink) { + lines.push(`**Documentation:** ${metadata.docsLink}`); + } + const flags: string[] = []; + if (metadata.isBYOC) flags.push("BYOC"); + if (metadata.isPro) flags.push("Pro"); + if (metadata.isHidden) flags.push("Hidden"); + if (flags.length > 0) { + lines.push(`**Flags:** ${flags.join(", ")}`); + } + return lines; +} + +/** + * Format a complete tool definition with all details + */ +function formatToolDefinition(tool: ToolDefinition): string[] { + const lines: string[] = []; + + // Tool header + lines.push(`### ${tool.qualifiedName}`); + lines.push(""); + + // Version info + if ( + tool.fullyQualifiedName && + tool.fullyQualifiedName !== tool.qualifiedName + ) { + const version = tool.fullyQualifiedName.split("@")[1]; + if (version) { + lines.push(`**Version:** ${version}`); + } + } + + // Description + if (tool.description) { + lines.push(""); + lines.push(tool.description); + } + + // Parameters + lines.push(...formatParameters(tool.parameters)); + + // Auth scopes + lines.push(...formatToolAuth(tool.auth)); + + // Secrets + lines.push(...formatSecrets(tool.secrets, tool.secretsInfo)); + + // Output + lines.push(...formatOutput(tool.output)); + + // Tool metadata + lines.push(...formatToolMetadata(tool.metadata)); + + // Code example with auth and secrets + lines.push( + ...formatCodeExample( + tool.codeExample, + tool.auth, + tool.secrets, + tool.secretsInfo + ) + ); + + lines.push(""); + + return lines; +} + +// ============================================================================ +// Main Export +// ============================================================================ + +/** + * Convert toolkit data to comprehensive markdown documentation + * Includes all available information: parameters, auth, secrets, examples + */ +export function toolkitDataToSearchMarkdown(toolkit: ToolkitData): string { + const title = toolkit.label || toolkit.id; + const sections: string[] = buildToolkitHeaderSections(toolkit, title); + sections.push(...formatToolkitAuth(toolkit.auth)); + sections.push(...buildToolkitMetadataSection(toolkit)); + sections.push(...buildDocumentationChunkSections(toolkit)); + + const toolsToInclude = toolkit.tools.slice(0, TOOL_LIMIT); + const truncatedCount = Math.max( + 0, + toolkit.tools.length - toolsToInclude.length + ); + + sections.push( + ...buildToolsSummarySections( + toolkit.tools.length, + toolsToInclude, + truncatedCount + ) + ); + sections.push(...buildToolDetailsSections(toolsToInclude)); + sections.push(...buildGeneratedAtSection(toolkit.generatedAt)); + + return sections.join("\n"); +} + +function buildToolkitHeaderSections( + toolkit: ToolkitData, + title: string +): string[] { + const sections: string[] = [`# ${title}`]; + + if (toolkit.version) { + sections.push(`**Version:** ${toolkit.version}`); + } + + if (toolkit.description) { + sections.push("", toolkit.description); + } + + if (toolkit.summary) { + sections.push("", "## Overview", "", toolkit.summary); + } + + return sections; +} + +function buildDocumentationChunkSections(toolkit: ToolkitData): string[] { + if ( + !toolkit.documentationChunks || + toolkit.documentationChunks.length === 0 + ) { + return []; + } + + const sections: string[] = []; + for (const chunk of toolkit.documentationChunks) { + if (chunk.content) { + sections.push("", chunk.content); + } + } + return sections; +} + +function buildToolsSummarySections( + totalTools: number, + toolsToInclude: ToolDefinition[], + truncatedCount: number +): string[] { + const sections: string[] = [ + "", + "## Tools", + "", + `This toolkit provides **${totalTools} tools**:`, + "", + "### Quick Reference", + "", + ]; + + for (const tool of toolsToInclude) { + sections.push(buildQuickReferenceLine(tool)); + } + + if (truncatedCount > 0) { + sections.push("", `*... and ${truncatedCount} more tools.*`); + } + + return sections; +} + +function buildQuickReferenceLine(tool: ToolDefinition): string { + const description = tool.description + ? ` — ${tool.description.split("\n")[0]}` + : ""; + const anchor = tool.qualifiedName.toLowerCase().replace(/\./g, ""); + return `- [\`${tool.qualifiedName}\`](#${anchor})${description}`; +} + +function buildToolDetailsSections(toolsToInclude: ToolDefinition[]): string[] { + const sections: string[] = ["", "---", "", "## Tool Details", ""]; + for (const tool of toolsToInclude) { + sections.push(...formatToolDefinition(tool), "---", ""); + } + return sections; +} + +function buildGeneratedAtSection(generatedAt?: string): string[] { + if (!generatedAt) { + return []; + } + return ["", `*Documentation generated: ${generatedAt}*`]; +} diff --git a/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts b/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts new file mode 100644 index 000000000..4639a4f9d --- /dev/null +++ b/toolkit-docs-generator/tests/scripts/generate-toolkit-markdown.test.ts @@ -0,0 +1,104 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ToolkitData } from "../../../app/_components/toolkit-docs/types"; +import { generateToolkitMarkdown } from "../../scripts/generate-toolkit-markdown"; + +const createToolkit = (overrides: Partial = {}): ToolkitData => ({ + id: "Github", + label: "GitHub", + version: "1.0.0", + description: null, + metadata: { + category: "development", + iconUrl: "https://design-system.arcade.dev/icons/github.svg", + isBYOC: false, + isPro: false, + type: "arcade", + docsLink: "https://docs.arcade.dev", + isComingSoon: false, + isHidden: false, + }, + auth: null, + tools: [], + documentationChunks: [], + customImports: [], + subPages: [], + generatedAt: new Date().toISOString(), + ...overrides, +}); + +const createTempDir = async (): Promise => + mkdtemp(join(tmpdir(), "toolkit-markdown-")); + +const cleanupTempDir = async (dir: string | null) => { + if (dir) { + await rm(dir, { recursive: true, force: true }); + } +}; + +describe("generateToolkitMarkdown", () => { + let tempDir: string | null = null; + + afterEach(async () => { + await cleanupTempDir(tempDir); + tempDir = null; + }); + + it("fails when no routes exist and failOnEmptyRoutes is true", async () => { + tempDir = await createTempDir(); + await expect( + generateToolkitMarkdown({ + routes: [], + outputRoot: tempDir, + failOnEmptyRoutes: true, + }) + ).rejects.toThrow("No toolkit routes found"); + }); + + it("writes markdown for available toolkits and skips missing data", async () => { + tempDir = await createTempDir(); + + const routes = [ + { category: "development", toolkitId: "github" }, + { category: "sales", toolkitId: "hubspot" }, + ]; + + const result = await generateToolkitMarkdown({ + outputRoot: tempDir, + routes, + readToolkitDataFn: (toolkitId: string) => { + if (toolkitId === "github") { + return Promise.resolve( + createToolkit({ id: "Github", label: "GitHub" }) + ); + } + return Promise.resolve(null); + }, + toMarkdown: () => "# Generated\n", + failOnEmptyRoutes: false, + }); + + const outputPath = join(tempDir, "development", "github.md"); + const content = await readFile(outputPath, "utf-8"); + + expect(content).toBe("# Generated\n"); + expect(result.written).toBe(1); + expect(result.skipped).toBe(1); + }); + + it("fails when no markdown is written and failOnEmptyRoutes is true", async () => { + tempDir = await createTempDir(); + + await expect( + generateToolkitMarkdown({ + outputRoot: tempDir, + routes: [{ category: "development", toolkitId: "github" }], + readToolkitDataFn: async () => null, + toMarkdown: () => "# Generated\n", + failOnEmptyRoutes: true, + }) + ).rejects.toThrow("No toolkit markdown generated"); + }); +}); From c95bacc32f624d15632ec0aa653f4c02ccfde1f1 Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 27 Feb 2026 16:42:42 -0300 Subject: [PATCH 10/11] fix: restore metadata filters in tool listing Reintroduce metadata filter controls for operations and behavior flags and wire them into tool filtering so metadata-based filtering appears and works in the available tools table. Made-with: Cursor --- .../components/available-tools-table.tsx | 382 +++++++++++++----- 1 file changed, 279 insertions(+), 103 deletions(-) diff --git a/app/_components/toolkit-docs/components/available-tools-table.tsx b/app/_components/toolkit-docs/components/available-tools-table.tsx index 3238ce096..9b1b947cf 100644 --- a/app/_components/toolkit-docs/components/available-tools-table.tsx +++ b/app/_components/toolkit-docs/components/available-tools-table.tsx @@ -240,6 +240,35 @@ export type AvailableToolsSort = | "secrets_first" | "selected_first"; +const BEHAVIOR_FILTER_LABELS: Record = { + readOnly: "Read only", + destructive: "Destructive", + idempotent: "Idempotent", + openWorld: "Open world", +}; + +const BEHAVIOR_FILTER_ORDER: BehaviorFlagKey[] = [ + "readOnly", + "destructive", + "idempotent", + "openWorld", +]; + +function formatMetadataLabel(value: string): string { + const words = value.split("_"); + return words + .map((word, index) => { + if (word === "crm") { + return "CRM"; + } + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + return word.toLowerCase(); + }) + .join(" "); +} + type SecretDisplayItem = { label: string; href?: string; @@ -599,13 +628,63 @@ export function AvailableToolsTable({ const [query, setQuery] = useState(""); const [filter, setFilter] = useState(defaultFilter); const [sort, setSort] = useState("name_asc"); + const [activeOperations, setActiveOperations] = useState>( + () => new Set() + ); + const [behaviorFlags, setBehaviorFlags] = useState< + Partial> + >({}); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [page, setPage] = useState(1); + const operationOptions = useMemo(() => { + const values = new Set(); + for (const tool of safeTools) { + for (const operation of tool.metadata?.behavior?.operations ?? []) { + if (operation.trim().length > 0) { + values.add(operation); + } + } + } + return [...values].sort((a, b) => a.localeCompare(b)); + }, [safeTools]); + + const behaviorFilterKeys = useMemo(() => { + const available = new Set(); + for (const tool of safeTools) { + const behavior = tool.metadata?.behavior; + if (!behavior) { + continue; + } + for (const key of BEHAVIOR_FILTER_ORDER) { + if (behavior[key] !== undefined) { + available.add(key); + } + } + } + return BEHAVIOR_FILTER_ORDER.filter((key) => available.has(key)); + }, [safeTools]); + + const hasMetadataFilters = + operationOptions.length > 0 || behaviorFilterKeys.length > 0; + const hasActiveMetadataFilters = + activeOperations.size > 0 || Object.keys(behaviorFlags).length > 0; + const filteredTools = useMemo(() => { - const filtered = filterTools(safeTools, query, filter); + const filtered = filterTools(safeTools, query, filter, { + activeOperations, + behaviorFlags, + }); return sortTools(filtered, sort, selectedTools); - }, [safeTools, query, filter, sort, selectedTools]); + }, [ + safeTools, + query, + filter, + sort, + selectedTools, + activeOperations, + behaviorFlags, + ]); const pageCount = useMemo( () => Math.max(1, Math.ceil(filteredTools.length / pageSize)), @@ -632,113 +711,210 @@ export function AvailableToolsTable({ return (
{(enableSearch || enableFilters) && ( -
- {enableSearch && ( -
- - { - setQuery(event.target.value); - setPage(1); - }} - placeholder={searchPlaceholder} - type="text" - value={query} - /> - {query && ( - + )} +
+ )} + {enableFilters && ( + <> + { - setFilter(value as AvailableToolsFilter); - setPage(1); - }} - value={filter} - > - + + + + All tools + + Requires OAuth scopes only + + No OAuth scopes + + Requires secrets only + + + No secrets required + + + + - + - - + {showSelection && ( + + Selected first + + )} + + + + )} + + + {filteredTools.length} + {" "} + of {safeTools.length} + +
+ + {enableFilters && hasMetadataFilters && ( +
+
+ + Metadata filters: + + {operationOptions.map((operation) => ( + + ))} + {behaviorFilterKeys.map((key) => ( + + ))} + {hasActiveMetadataFilters && ( + + )} +
+
)} - - - {filteredTools.length} - {" "} - of {safeTools.length} -
)} {filteredTools.length === 0 ? ( From 1894908ef2b2b6e19f33f8ce5d91a8ebdab0e66a Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 27 Feb 2026 22:40:24 -0300 Subject: [PATCH 11/11] fix: improve metadata filter chip UX and toolbar stability Group operations and behavior into a cleaner metadata filter row, keep boolean behavior filters as clickable chips with distinct Any/Yes/No styling, and stabilize the controls count area so toolbar layout doesn't shift as result totals change. Made-with: Cursor --- .../components/available-tools-table.tsx | 275 ++++++++++++------ 1 file changed, 185 insertions(+), 90 deletions(-) diff --git a/app/_components/toolkit-docs/components/available-tools-table.tsx b/app/_components/toolkit-docs/components/available-tools-table.tsx index 9b1b947cf..c163d541d 100644 --- a/app/_components/toolkit-docs/components/available-tools-table.tsx +++ b/app/_components/toolkit-docs/components/available-tools-table.tsx @@ -269,6 +269,178 @@ function formatMetadataLabel(value: string): string { .join(" "); } +type BehaviorFlagsState = Partial>; + +function getBehaviorFilterStateLabel(value: boolean | undefined): string { + if (value === undefined) { + return "Any"; + } + if (value) { + return "Yes"; + } + return "No"; +} + +function getNextBehaviorFilterValue( + value: boolean | undefined +): boolean | undefined { + if (value === undefined) { + return true; + } + if (value) { + return false; + } + return; +} + +function getBehaviorFilterToneClass(value: boolean | undefined): string { + if (value === undefined) { + return "border-muted/60 bg-neutral-dark/20 text-muted-foreground hover:bg-neutral-dark/35"; + } + if (value) { + return "border-emerald-500/45 bg-emerald-500/15 text-emerald-200 hover:bg-emerald-500/25"; + } + return "border-rose-500/45 bg-rose-500/15 text-rose-200 hover:bg-rose-500/25"; +} + +function BehaviorFilterButtons({ + behaviorFilterKeys, + behaviorFlags, + onBehaviorFlagsChange, + onFiltersChanged, +}: { + behaviorFilterKeys: BehaviorFlagKey[]; + behaviorFlags: BehaviorFlagsState; + onBehaviorFlagsChange: ( + updater: (current: BehaviorFlagsState) => BehaviorFlagsState + ) => void; + onFiltersChanged: () => void; +}) { + return behaviorFilterKeys.map((key) => { + const currentValue = behaviorFlags[key]; + const stateLabel = getBehaviorFilterStateLabel(currentValue); + + return ( + + ); + }); +} + +function MetadataFilterSection({ + operationOptions, + activeOperations, + onActiveOperationsChange, + behaviorFilterKeys, + behaviorFlags, + onBehaviorFlagsChange, + hasActiveMetadataFilters, + onFiltersChanged, +}: { + operationOptions: string[]; + activeOperations: Set; + onActiveOperationsChange: ( + updater: (current: Set) => Set + ) => void; + behaviorFilterKeys: BehaviorFlagKey[]; + behaviorFlags: BehaviorFlagsState; + onBehaviorFlagsChange: ( + updater: (current: BehaviorFlagsState) => BehaviorFlagsState + ) => void; + hasActiveMetadataFilters: boolean; + onFiltersChanged: () => void; +}) { + return ( +
+
+ {operationOptions.length > 0 && ( +
+ + Operations + + {operationOptions.map((operation) => ( + + ))} +
+ )} + {behaviorFilterKeys.length > 0 && ( +
+ + Behavior + + +
+ )} + {hasActiveMetadataFilters && ( + + )} +
+
+ ); +} + type SecretDisplayItem = { label: string; href?: string; @@ -631,9 +803,7 @@ export function AvailableToolsTable({ const [activeOperations, setActiveOperations] = useState>( () => new Set() ); - const [behaviorFlags, setBehaviorFlags] = useState< - Partial> - >({}); + const [behaviorFlags, setBehaviorFlags] = useState({}); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [page, setPage] = useState(1); @@ -820,100 +990,25 @@ export function AvailableToolsTable({ )} - + {filteredTools.length} {" "} - of {safeTools.length} + of {safeTools.length} tools
{enableFilters && hasMetadataFilters && ( -
-
- - Metadata filters: - - {operationOptions.map((operation) => ( - - ))} - {behaviorFilterKeys.map((key) => ( - - ))} - {hasActiveMetadataFilters && ( - - )} -
-
+ setPage(1)} + operationOptions={operationOptions} + /> )}
)}